Closed oed closed 2 years ago
Replying to @kdenhartog from the duplicate discussion thread #2851
Original message:
My first round of feedback for this:
One of the general topics that's been discussed quite a bit is if we want to restrict the algorithms and key types supported in the JOSE registries. Often times this is done to limit the scope of code that needs to be written by various implementations which in turn should bolster interoperability. I see you've done this via recommendations, but I'd wonder if we want to restrict further usage here via explicitly not supporting certain algorithms. Is that something you think would be beneficial?
Note: This next piece of feedback is based on potentially misunderstanding the paths permission aspect. I'd like to figure out more about this and then we may not need to address the comments below. Additionally, I'd like to understand the architecture of where the operations are being performed because I'm thinking I misunder
Another point that I caught quite quickly is that this EIP is looking to use DIDs to manage discoverability of keys. I'm happy to see this approach being taken further as I think it will help quite a bit. I hope it's an approach that more DApps look to use as well. With that in mind, one of the things that I wasn't sure about was how the API would be integrated with key management to perform the sign and encrypt operations. Is your current assumption that a Key Management architecture will be used within the implementation and it's the responsibility of implementers to be handle this? In general, every JOSE implementation I've looked at has made the assumption that the private key is provided in some way and it will perform the cryptographic operation when necessary. This is slightly different from what's proposed at this point though. I think we may need to think more about what the general expectation is on the implementers and the callers in terms of who's providing which data (private key), who's performing the cryptographic operations, and who's performing the structuring of the data (e.g. creating the JWE with the signed data). From there we'll likely want to take a look at the general API design to make sure it aligns with what's generally done by wallets and other DApps that would be using this API.
I'm also interested to know more about why encrypt and verify were not included. I assume there's back history from the previous attempts that may further explain this. Have they already been added via other APIs or was there some other reason that they were left off?
In general though @oed, I like the direction that you're going with this and I'm curious to hear others opinions on this.
Personally I would be fine with restricting the alg and key types to the ones that are supported by JOSE registries (are those used by default for COSE as well?).
So the JSON-RPC is used for two main things with ethereum. Interacting interacting with chain state, and requesting things from the wallet. This means that it acts as a way for an app to request signatures and account information from a wallet. So in our case here the only things needed to be done wallet side is signing and decrypting. Internally wallets are free to use whichever key management system they prefer. A caller of any of these methods won't need to provide any type of key material. Both verifying and encryption can happen completely client side (without any wallet interaction) so there is no need to add methods for them. Instead a utility library that does this can be implemented (we're thinking about implementing these utility functions in the js-did library).
Things to consider...
secp256k1 supports ecdsa (ES256K JWS) and Schnorr (SS256K (not registered!)).... and UPort created ES256K-R
which is similarly not registered... and it identical to ES256K
with appended recovery byte...
IMO, making the header a required RPC argument, and restricting the valid values of alg
is a good way of addressing this issue.
There is also detached JWS https://tools.ietf.org/html/rfc7797 ... this is really useful for when you are signing binary... not some JSON.stringify-iable data (lots of issues with canonicalization and JOSE)....
It's also the default JWS format used by Linked Data Proofs.
I'm not sure adding the did_*
prefix to RPC calls is wise.... especially if what we are really doing is exposing JOSE / COSE interfaces.... I would prefer to see jose_jws_sign
and jose_jws_sign_detached
... and the COSE equivalents.
Thanks @OR13
secp256k1 supports ecdsa (ES256K JWS) and Schnorr (SS256K (not registered!)).... and UPort created ES256K-R which is similarly not registered... and it identical to ES256K with appended recovery byte...
Good point. I should probably explicitly say ES256K
👍
IMO, making the header a required RPC argument, and restricting the valid values of alg is a good way of addressing this issue.
Hm, seems like this would require the application developer to be aware of which algs the wallet supports. This would mean that there needs to be an additional rpc method for getting this information. I'd rather have the wallet decide this and append the alg that it is using.
There is also detached JWS https://tools.ietf.org/html/rfc7797
Happy to add this if others express interest.
I'm not sure adding the did_* prefix to RPC calls is wise.... especially if what we are really doing is exposing JOSE / COSE interfaces.... I would prefer to see jose_jws_sign and jose_jws_sign_detached... and the COSE equivalents.
The whole point here is to enable DID signed JOSE/COSE objects. Essentially by setting the kid
to the DID (and key fragment) for the signing entity. Imo this is critical for this EIP to be successful. Using did_*
for decryption also simplifies things because the combination of key discovery and decryption is key for dev UX.
If the developer is not required to know which signing algorithms are supported and set the header explicitly, a default header should be provided.... which defaults will be used?
+1 to using kid
and making its default value of the form did:example:123#fragment
what about all the developers who want JWTs / JWSs but don't care about DIDs?... I agree that the APIs should be simple, but i question why they should start with dids... to me... dids are more of an option for setting the kid
.... but I am assuming there would be APIs for retrieving JWKs.... so the entire system can be used with or without DIDs.
which defaults will be used?
That's up to the wallet implementer (a reccommendation is provided in the EIP)
but I am assuming there would be APIs for retrieving JWKs
What do you mean by this exactly? To me this is what you would use the DID for. Using did:ethr
would be the most straight forward way for wallets to support some way of distributing JWKs, as all ethereum wallets support did:ethr
already.
put another way...
what RPC interfaces will be provided for getting public key bytes....
possibly options:
did_resolve
-> didDocument as json
jose_get_jwk_by_kid
-> JWK
did_jws_verify
vs jose_jws_verify
For example... Tangem cards support a "getPublicKey" NFC interface. You can then use that interface to verify signatures... the cards themselves don't support "verifying on the card".... thats similar to the concept of "verifying on the rpc server"...
I'd prefer to have a JOSE RPC interface, that happened to work with DIDs... and a DID RPC interface that supported things like:
resolution (which returns a set of keys, and their associated relationships)...
those relationships have nothing to do with JOSE... they are no mentioned in the JOSE RFCs... they were created in DID Core.
what RPC interfaces will be provided for getting public key bytes
This is what did_authenticate
is for. With it you will get back the DID that was authenticated. To get the public key bytes you can use something like the DID Resolver which works with any DID method (provided a resolver is implemented for it).
The problem with an RPC method like did_resolve
is that it's not really the responsibility of a wallet to resolve DIDs for apps. Instead Ethereum clients would need to implement this method, but then there has to be consensus among client implementers which DID methods to support (would be very hard). I also think DID resolution is more of an application on top of blockchains rather than something that should be implemented in blockchain client software.
did_authenticate
should return a proof-of-control of the DID and should expect an aud
/ domain
and nonce
/ challenge
parameter. I don't know which data format the proof should have and whether it would be allowed to support different formats but it could be either a JWS or CWS.
Updated:
You might be able to use the path
as the challenge.
I guess did_createCWS
should return a CWS, so there might be a typo in the return value.
Returns: A CWE encoded as base64, string
^^should be rather CWS, right?
Is there a reason to not include an API for:
did_createCWE
did_createJWE
?
Update:
That certainly only makes sense using authcrypt -> e.g., ECDH-1PU
Is the did
parameter needed if the kid
or something similar could be used to determine the DID in the CWE/JWE?
Is it needed to recommend a decryption algorithm for decryption? The decryption algorithm can be determined from the CWE/JWE directly, trying to decrypt the CWE/JWE with a different algorithm, would fail anyways. In general I agree that ECDH-ES (X25519) + XChacha20Poly1305 should be recommended.
did_authenticate
should return a proof-of-control of the DID and should expect anaud
/domain
andnonce
/challenge
parameter. I don't know which data format the proof should have and whether it would be allowed to support different formats but it could be either a JWS or CWS. This definitely would be super useful and best practice that should be adopted by all Ethereum wallets IMO
Me and @wighawag drafted a similar proposal last year where accounts would be exposed with signatures for a given challenge.
https://ethereum-magicians.org/t/automatic-authentication-signature/2429
If this pattern would be possible with did_authenticate
by default then we could replace the current eth_requestAccounts
with this method instead to include proof-of-control
Same dapps currently after getting the user's accounts follow up with a personal_sign
request to attest for the private key ownership or even to access the public key of the account.
I think this should just be the default behavior for all dapp and wallet interoperability
Adding a challenge makes sense! Thanks for suggesting @awoie and clarifying @pedrouid. Will add this when I update the draft. Might make sense to make this an actual JWT/CWT with an expiry actually. A challenge
should definitely be included.
What is your thinking on aud
@awoie? Is it strictly needed?
I guess did_createCWS should return a CWS, so there might be a typo in the return value.
Nice catch!
That certainly only makes sense using authcrypt -> e.g., ECDH-1PU
Would you mind expanding on this? And yeah for the suggested encryption alg you can just do the encryption client side.
Is it needed to recommend a decryption algorithm for decryption?
It's really up to the wallet to decide what to use, and state that in their DID document.
Is the did parameter needed if the kid or something similar could be used to determine the DID in the CWE/JWE?
The kid
should not specified in the JWE/CWE. That would leak information about the ciphertext.
Adding a challenge makes sense! Thanks for suggesting @awoie and clarifying @pedrouid. Will add this when I update the draft. Might make sense to make this an actual JWT/CWT with an expiry actually. A
challenge
should definitely be included.
nonce
has that purpose in id_token JWTs in OIDC (see IANA claims registry). If you introduce a new claim for this purpose, then registering a new claim name in the IANA should be considered.
What is your thinking on
aud
@awoie? Is it strictly needed?
aud
can be optional and it can be any arbitrary stringOrUri. I think it would be good practice to make use of that. Everyone that receives the JWS would have to identify itself and then decide whether to reject the JWS. Not sure what is the CWS equivalent.
That certainly only makes sense using authcrypt -> e.g., ECDH-1PU
Would you mind expanding on this? And yeah for the suggested encryption alg you can just do the encryption client side.
ECDH-1PU is for example used in DIDComm-messaging to encrypt a message to a DID while enabling the receiver of the encrypted message to verify some sort of authenticity that the sender is has a certain DID (or more general keys) by avoiding the sign-JWS-then-encrypt-JWE pattern. In that case, only the wallet would be able to create such a JWE/CWE because only the wallet controls the private key.
Is the did parameter needed if the kid or something similar could be used to determine the DID in the CWE/JWE?
The
kid
should not specified in the JWE/CWE. That would leak information about the ciphertext.
The kid
won't be in the Ciphertext of the JWE. It would be in the unprotected header field per recipient: see https://tools.ietf.org/html/rfc7516#section-4.1.6. You could use the same pattern as you described for JWS/CWS and use a DID URL.
nonce
has that purpose in id_token JWTs in OIDC (see IANA claims registry).
Great, let's use nonce
👍
aud
can be optional and it can be any arbitrary stringOrUri.
Ok, so this would be optionally set by the wallet?
ECDH-1PU is for example used in DIDComm-messaging to encrypt a message to a DID while enabling the receiver of the encrypted message to verify some sort of authenticity that the sender is has a certain DID (or more general keys) by avoiding the sign-JWS-then-encrypt-JWE pattern.
Makes sense, but seems a bit out of scope for now. Could easily be addressed in an additional EIP! Q: Is the receiver the only party that can verify sender authenticity or can everyone do it?
The
kid
won't be in the Ciphertext of the JWE.
That's my point :) In some cases you might not want to leak information about whom the ciphertext is intended for.
kid
is optional everywhere, but for JWS, its pretty critical for handling resolution of public key bytes for verification, see this list of JOSE /. DID issues:
https://github.com/decentralized-identity/did-jose-extensions/blob/master/options.md
TL;DR; I'd rather see use of kid
defined consistently as pointing to a verificationMethod
id
... than see people keep creating 1 off spec text to handle key lookups without using JSON-LD.
I agree we should postpone encryption until we have signatures. can we take an inventory of the proposed RPC methods?
There is also detached JWS https://tools.ietf.org/html/rfc7797 ... this is really useful for when you are signing binary... not some JSON.stringify-iable data (lots of issues with canonicalization and JOSE)....
@oed @OR13 Does it make sense to also support JWS JSON serialization? What happens if a DID has more than one signature key attached to it?
I assume the API interface would support specifying the exact key to use for signing....
For example: https://github.com/transmute-industries/did-key.js/blob/master/packages/secp256k1/src/__fixtures__/index.ts#L58
did:example:123#fragment
is a URI that identifies the public "verification side" of the private "signing side"...
I would consider a wallet API that didn't let me choose the signing key pretty broken, in all cases where there was more than 1 signing key.... its not clear to me that is the case for did:ethr
or secp256k1 did:key
however... since the did identifies a particular public / private key pair.
I assume the API interface would support specifying the exact key to use for signing....
For example: https://github.com/transmute-industries/did-key.js/blob/master/packages/secp256k1/src/__fixtures__/index.ts#L58
did:example:123#fragment
is a URI that identifies the public "verification side" of the private "signing side"...I would consider a wallet API that didn't let me choose the signing key pretty broken, in all cases where there was more than 1 signing key.... its not clear to me that is the case for
did:ethr
or secp256k1did:key
however... since the did identifies a particular public / private key pair.
With JSON Serialization you would be able to send multiple signatures back in case the DID controls a few signing keys. That was why I brought that up.
@awoie hm so the interface would be kid
: array<did_uri>
, payload: any
, header: any
=> json serialized JWS
Seems useful.
TL;DR; I'd rather see use of kid defined consistently as pointing to a verificationMethod id...
Makes sense. This was my intention. Maybe that was unclear?
Does it make sense to also support JWS JSON serialization? What happens if a DID has more than one signature key attached to it?
I'm agnostic as to which serialization to use. I'm curious though what the use case is for signing with multiple keys from the same DID? If you want to have two different DIDs sign the same payload you can just take two JWS with compact serialization and reserialize them using the JSON serlialization (which is trivial).
I assume the API interface would support specifying the exact key to use for signing....
Yeah exactly. You can already specify the fragment in the did
param of did_createJWS
.
take two JWS with compact serialization and reserialize them
Would this break the signatures of the two because of the non-payload data being signed (e.g. protected headers)?
If not, it would be cool to enable multi-signature supports as a separate API where a JWS can be passed in with the DID and it will de-assemble the JWS and sign the payload/header data and then that JSON serialized JWS could be passed as a form of a multi-signature to something like a smart contract or any other verifier for that matter.
Thanks for creating this proposal.
Regarding did_authenticate
. From the discussion, It looks like it has multiple purpose
The current draft of this proposal specify that did_authenticate
requires user permission. This seems important if authentication is used for something other than ensuring that the user indeed control the did (the private key of the ethereum address presented).
Otherwise, as @pedrouid mentions, did_authenticate
could simply replace eth_requestAccounts
, see in particular: https://ethereum-magicians.org/t/eip-1102-opt-in-provider-access/414/58 which would already require user to accept showing their did.
But looking at the current draft of the proposal here, it seems that did_authenticate
performs more than simply checking if the user control the did being advertised.
It accept a path
parameter. And then further in the proposal it is mentioned that decryption happen without user confirmation only if did_authenticate
was already accepted for a particular path.
If we go with the idea that did_authenticate
replace eth_requestAccount
, the path
should default to the application origin (like in eth_requestAccount
) then the only prompt should be about accepting to reveal your did, the authentication is then done automatically so the application can be sure the user is indeed in control of that did and is not someone faking it.
Any other path
would require user confirmation.
Furthermore as mentioned here and explained in Proposal 1: Non-Interactive Decryption
If we consider the default path being the application origin,
Then decryption should happen automatically for every encryption performed from that same origin. No user confirmation should be required there. For that a mechanism should be put in place so that encryption include the origin
in the data.
This would allow a seamless encrypted storage for application, that can then be safely shared across devices. Of course this is only fully under user control for application on content-addressable system like ipfs, but as mentioned in my comment and blog, an ENS or even DNS name that point to an ipfs hash should use the ipfs hash as the origin (not the ENS name).
Now if the content change (and so the uri), the user would be requested to did_authenticate
the new "path"
Now, I am not very knowledgeable with JWS and I might abuse term "path" here by allowing it to represent URI. If that is the case, we could separate the 2.
Note: for traditional URI (non content-addressable) it could be up to the wallet to decide whether a user confirmation is required as there is a security risk that the application get compromised.
Would this break the signatures of the two because of the non-payload data being signed (e.g. protected headers)?
It wouldn't. Each signer is free to choose what to put in their protected header. For example they would need to put their own kid
in there.
@wighawag While we could use path
like that I would strongly advise against it since it would incentivize apps to silo data around their origins. Where as if you just use a path unrelated to origin this would make it easier to share data across apps.
I'm saying this mainly because I believe data should always be independent of the app that is bee used to display the data.
@oed I agree that we should not silo data. But here the proposal is to remove the need for approval for the default case. This is better for the uers and that's why I think it should be that way. No reason to request a confirmation when the application that request decryption was the one encrypting it. Other application can alwasy request user to approve them for that origin. not big deal
@wighawag i don't think the UX of approving both access to the account and a path is worse than just the account. It also makes it clear for the user what's going on. Origins also doesn't make sense for mobile apps.
@oed I personally think it does make it worse, It could also lead to authorization fatigue
Re mobile app, this is not true. application has also some unique identifier. And for case where getting a unique identifier is not possible, the permission request would show up. We should not sacrifice one use case because it is not possible in all scenario.
I should precise though that this not for all use case. You would obviously not use an ipfs hash as path for data that you expect to be globally shared like name, profile picture, etc
This is mostly for specific data that would be a pain to request authentication for.
But maybe, a better alternative is to request authentication at the time of encryption (even if is is technically unnecessary) so a user is not surprised to have to accept an application after already interacting with it.
But I guess the application could simple call did_authenticate
before hand and there would be no issue.
My aim is to remove all unnecessary confirmation, as this is a real pain from the user perspective.
Actually if did_authenticate
replace eth_requestAccount
as @pedrouid mentioned, then this get reduced to one confirmation.
And this is I guess what you @oed were referring to with UX of approving both access to the account and a path
I agree it would be nice to prevent authorization fatigue. I think there are 2 main cases for calling did_authenticate
:
paths
in order to provide its basic functionality, and prompts for the user consent when starting the app, similar to a login prompt.I think beyond the paths
, it would be useful to add more granular permissions the user could have control over:
session
or permanent
So rather than having a paths
field in did_authenticate
, we could have a grants
array with the following shape:
type Grant = {
path: string
access: {
create?: boolean
read?: boolean
update?: boolean
delete?: boolean
}
duration?: 'session' | 'permanent'
}
An example "login" prompt could be something like:
{
did: "...",
grants: [
{ path: "/profiles", access: { read: true }, duration: "permanent" },
{ path: "/media/pictures/albums/<id>", access: { read: true, update: true }, duration: "permanent" }
]
}
Then a subsequent grant to edit profile for the session duration could be:
{
did: "...",
grants: [
{ path: "/profiles/basic", access: { update: true }, duration: "session" }
]
}
A one-off "add album" prompt could be:
{
did: "...",
grants: [
{ path: "/media/pictures/albums", access: { create: true } }
]
}
This way it wouldn't be an "all or nothing" choice for the user, but rather a more similar experience to what mobile OS are already providing to some extend.
@PaulLeCam are you implying that did_createJWs
also relies on path
somehow? How do you imagine that working?
I don't know enough about DID to have a good idea of how it should work, but I'd imagine if the JWS identifies a known resource, the user agent could check the user grants?
I'm struggling to understand why we have moved from JOSE APIs for DIDs to ABAC... I assume it's because there is no permissions model for accessing those APIs? Last time I used JSON-RPC it was game over if you accidentally exposed it to the internet... what changed that we need a permissions model?
Yes sorry that's probably out of scope for this EIP, should be another discussion.
I suggest that for simplicity and ease of implementation we reduce this EIP to only include JOSE related method and remove the COSE related ones. These can be added as an additional EIP at some later point.
I will follow up in the next few days with a full list of modifications to this EIP based on the current feedback, and then follow up with a new PR that modifies the EIP.
@oed Thanks for your work on this. I'm confused about a few of the pieces and have concerns which might be just due to my own confusion.
My largest concern is portability. It looks to me like so much of the implementation is left up to the individual wallets that it will be difficult to port between wallets even if they both implement this EIP. Could you explain some about how you see backing up and porting data working?
Great questions @wilwade!
did_*
methods described here and be compatible.did:ethr
.I think leaving the implementation up to wallets is a key aspect of this EIP, since we want to support different types of DID methods. It would however be a really good idea to standardize key management around particular DID methods!
List of updates to be added to the EIP soon:
did_authenticate
method. The request should include a nonce
. The response must now include a signed JWS that includes the nonce
and optionally aud
.did
param of did_createJWS
to kid
which now takes an array of DID-urisPlease let me know if any of the above doesn't make sense of if I'm missing something. Will be making the updates to the EIP shortly.
- Add a challenge to the ´did_authenticatemethod. The request should include anonce. The response must now include a signed JWS that includes the nonceand optionallyaud`.
Are you thinking that the JWS will essentially be a signed JWT by following the claims format of the payload in JWTs?
@kdenhartog It could be. I don't really have a preference and would opt for simplicity. Do you have any suggestion of what would be optimal?
I guess just creating a JWT compatible with did-jwt could make sense?
I guess it depends on what the intent of the architecture is meant to be more of a messaging system then I'd lean towards the JWM stuff we've been working on. This comes with the advantage of getting closer alignment to the DIDCommV2 work as well.
Interesting @kdenhartog what would the payload and header look like if we would use JWM? Would also be interested in input from @awoie here!
Something that came up as I've been looking closer at JWE encryption is that the spec never mentions how to do padding of the cleartext before doing the encryption. Is there a recommended way of doing this or is it simply left to the application layer?
Perhaps you have some insights @kdenhartog @OR13 @awoie @selfissued ?
Thanks @OR13, but I can't find anything on cleartext padding anywhere in there.
https://crypto.stackexchange.com/questions/40731/minimum-plaintext-length-for-aes-gcm
@oed I guess it probably depends on the JWE algorithm, for example https://github.com/StableLib/stablelib/blob/master/packages/aes-kw/aes-kw.test.ts
https://github.com/panva/jose/blob/1876669a192e0c5d6b8c29a7408a8a8128d0b8b1/test/jwe/sanity.test.js
If you are implementing your own JWE, best to start with conformance tests against another library, and a set of test vectors committed to source control (that you pulled / generated from their implementation if possible).
Thanks, I did actually implement JWE here: https://github.com/decentralized-identity/did-jwt/pull/132 using xchacha20poly1305
and xC20PKW
with test vectors generated by the panva/jose library. However it seems like that does not pad the cleartext in any way.
I observed both the panva/jose
library as well as the digitalbazaar/minimal-cipher
library and could not see that the cleartext gets padded anywhere. The reason I ended up with doing a new implemetation was because:
panva/jose
only supports xchacha if you use a dependency that pulls in libsodium as a wasm moduledigitalbazaar/minimal-cipher
uses xchacha20poly1305
for the symmetric cipher and A256KW
for key wrapping, which I find super weird since it mixes two different ciphersECDH-ES+A256KW
works with keys other than x25519... I added support for the NIST Curves, and could add support for secp256k1.
I suppose you are right that this combination of algorithms is a bit complex, but.... it works 'out of the box' with many different key types: https://github.com/w3c-ccg/lds-jws2020/search?q=ECDH-ES%2BA256KW
in general, "new crypto" is "poorly supported" :)
I think the important part is supporting 1 older stable option, and 1 new (believed to be safer / faster) option. (stole this line from manu).
As the suite author those choices are up to you.
On my list of todos is to add secp256k1 support for producing JWE or type ECDH-ES+A256KW
... that use xchacha20poly1305
.... if thats a thing you are also interested in doing thats great.
if you plan to use something else, I would love to hear why...
This post frequently gets raised: https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-bad-standard-that-everyone-should-avoid
everyone has their own favorite cryptos to use, the important part is to make it clear which standards you support in your library and beware that not supporting old / new crypto, might lead folks to not use your tooling / fork it to add backwards / forwards compatibility.
ping @msporny @dlongley :)
https://eips.ethereum.org/EIPS/eip-2844