Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support for did:jwk and p-256, p-384, p-512 #1446

Merged
merged 8 commits into from
May 11, 2023

Conversation

TimoGlastra
Copy link
Contributor

No description provided.

Signed-off-by: Timo Glastra <timo@animo.id>
@TimoGlastra TimoGlastra requested a review from a team as a code owner May 1, 2023 17:04
Signed-off-by: Timo Glastra <timo@animo.id>
@codecov-commenter
Copy link

codecov-commenter commented May 1, 2023

Codecov Report

Merging #1446 (6401107) into main (7dd4061) will decrease coverage by 9.69%.
The diff coverage is 34.61%.

@@            Coverage Diff             @@
##             main    #1446      +/-   ##
==========================================
- Coverage   85.55%   75.86%   -9.69%     
==========================================
  Files         886      848      -38     
  Lines       21156    20434     -722     
  Branches     3635     3524     -111     
==========================================
- Hits        18100    15503    -2597     
- Misses       2881     4593    +1712     
- Partials      175      338     +163     
Impacted Files Coverage Δ
packages/core/src/crypto/keyUtils.ts 75.00% <ø> (ø)
...s/core/src/modules/dids/domain/key-type/ed25519.ts 68.42% <ø> (-27.04%) ⬇️
...ds/domain/verificationMethod/VerificationMethod.ts 100.00% <ø> (ø)
packages/core/src/crypto/EcCompression.ts 10.41% <10.41%> (ø)
...re/src/modules/dids/methods/jwk/JwkDidRegistrar.ts 16.00% <16.00%> (ø)
...ckages/core/src/modules/dids/methods/jwk/DidJwk.ts 17.85% <17.85%> (ø)
.../src/modules/dids/methods/jwk/didJwkDidDocument.ts 30.76% <30.76%> (ø)
...ges/core/src/modules/dids/domain/keyDidDocument.ts 50.94% <33.33%> (-49.06%) ⬇️
...ore/src/modules/dids/methods/jwk/JwkDidResolver.ts 33.33% <33.33%> (ø)
packages/core/src/crypto/JwkTypes.ts 35.29% <35.29%> (ø)
... and 15 more

... and 217 files with indirect coverage changes

Copy link
Contributor

@berendsliedrecht berendsliedrecht left a comment

Choose a reason for hiding this comment

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

Really nice addition! Left some remarks, nothing that should be difficult to resolve.

packages/askar/src/utils/askarKeyTypes.ts Show resolved Hide resolved
packages/core/src/crypto/EcCompression.ts Outdated Show resolved Hide resolved
packages/core/src/crypto/EcCompression.ts Outdated Show resolved Hide resolved
packages/core/src/crypto/EcCompression.ts Show resolved Hide resolved
// p-256, p-384, p-521
if (isP256Jwk(jwk) || isP384Jwk(jwk) || isP521Jwk(jwk)) {
// TODO: do we want to use the compressed key in the Key instance?
const publicKeyBuffer = Buffer.concat([TypedArrayEncoder.fromBase64(jwk.x), TypedArrayEncoder.fromBase64(jwk.y)])
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay so I did some digging and we should a check for uncompressed points.

If the prefix is 0x3 or 0x2, the key is uncompressed and we do not support that. Or we would have to add support, which is relatively easy, but maybe unnecessary.

this also means that we would have to add the 0x4 prefix when using compressed.

Sorry for the lack of sources, I will do my best to find 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.

I think the did:key spec only supports the compressed variant (that's what I understand from the thread), but you're saying the JWK itself could contain both compressed and uncompressed variants? I think we could just support uncompressed as well (as it's just skipping the compression/uncompression and thus simpler).

We'll always encode to compressed variant (because why not), but for decompression I should just look if the key is prefixed with 0x3 or 0x2 and if so, remove that previx and not do the decompression?

Copy link
Contributor

Choose a reason for hiding this comment

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

AFAIK the JWK spec does not allow for compressed values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm in that case, there's no reason to support the uncompressed variant I think? did:key only supports compressed, and the jwk will have separate values (x and y), not a single concatenated public key

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed. We can always add uncompressed later on, if needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

AFAIK the JWK spec does not allow for compressed values.

The JWK spec does very much support compressed and uncompressed. We are talking specifically about key representation and not the compressed format of a jwt, etc.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the did:key spec only supports the compressed variant (that's what I understand from the thread), but you're saying the JWK itself could contain both compressed and uncompressed variants?

No, I mean that we should check whether it is compressed or uncompressed, based on the prefix, and handle accordingly. We will break keys if this mechanism is implemented incorrectly which will lead to a lot of errors.

I think we could just support uncompressed as well (as it's just skipping the compression/uncompression and thus simpler).

Yes! I don't think it's too much effort.

We'll always encode to compressed variant (because why not), but for decompression I should just look if the key is prefixed with 0x3 or 0x2 and if so, remove that previx and not do the decompression?

Yes, I would have to look at the exact algo for this, but I think that's 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.

@blu3beri I've addressed all comments except the ones related to compression of P keys. Can you help me out a bit in pointing out what I need to exactly?

}
}

if (key.keyType === KeyType.P256 || key.keyType === KeyType.P384 || key.keyType === KeyType.P521) {
Copy link
Contributor

Choose a reason for hiding this comment

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

So this one would also need to account for the prefix.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not really right? We could just always output the compressed variant, but support both variants the other way around

Copy link
Contributor

Choose a reason for hiding this comment

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

The compressed variant, using only the X coordinate for the representation needs a prefix in the jwk of 0x03 or 0x02 I think depending on wether the y coordinate is odd or even (but that's from memory, would need a source but I am fairly sure). If we encode it without a prefix it will be interpreted incorrectly by the receiver.

packages/core/src/crypto/JwkTypes.ts Outdated Show resolved Hide resolved
packages/core/src/crypto/JwkTypes.ts Outdated Show resolved Hide resolved
packages/core/src/crypto/keyUtils.ts Outdated Show resolved Hide resolved
Co-authored-by: Berend Sliedrecht <61358536+blu3beri@users.noreply.github.com>
Signed-off-by: Timo Glastra <timo@animo.id>
packages/core/src/crypto/EcCompression.ts Outdated Show resolved Hide resolved
packages/core/src/crypto/JwkTypes.ts Outdated Show resolved Hide resolved
x: string
y?: string
use?: 'sig' | 'enc'
Copy link
Contributor

Choose a reason for hiding this comment

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

What do we do if the use param isn't provided? According to the spec we can enforce it:

4.2. "use" (Public Key Use) Parameter

The "use" (public key use) parameter identifies the intended use of
the public key. The "use" parameter is employed to indicate whether
a public key is used for encrypting data or verifying the signature
on data.

Values defined by this specification are:

o "sig" (signature)
o "enc" (encryption)

Other values MAY be used. The "use" value is a case-sensitive
string. Use of the "use" member is OPTIONAL, unless the application
requires its presence.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this depends on how purely we want to follow the spec. If we go 100%, we need to allow arbitrary string values as well. If not, I think we should maybe enforce the user to specify its value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For now, we just use it in parsing. It's required to be understood (not required to be present) for did:jwk. So we use when handling jwks, but only when we parse a JWK from someone else. When we create a JWK, for now I just always use the full capabilities of a key, and we don't include the use key. That's to to keep it 'simpler' (support the spec, but don't expose all functionality on our side).

I can update it to be a 'enc' | 'sig' | string value

Copy link
Contributor

Choose a reason for hiding this comment

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

What do you mean by "required to be understood"?

I'm fine with keeping it as is as long as we actually use it. Adding the string type without adding logic to handle that doesn't make sense to me.

I was just wondering why we include it, as it's optional, only used for incoming JWKs, and only used by the forEncrypting and forSigning getters. Can we actually use those getters (as the use value is optional)?

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 was just wondering why we include it

Because it's required by the did:jwk spec:

If the JWK contains a use property with the value "sig" then the keyAgreement property is not included in the DID Document. If the use value is "enc" then only the keyAgreement property is included in the DID Document.
The JWK should have the appropriate use value set to match the capabilities of the specified crv. For example, the
curve ed25519 is only valid for "sig" use and X25519 is only valid for "enc" (see RFC 8037 and the second example below).
The JWK may contain additional custom properties and values which will be accessible only in the verificationMethod. Any additional properties other than use (as documented above) are not referenced or used in the generation of the DID Document.

So we use it in the minimal way possible. When we parse a did:jwk we do the following:

  1. If no use property is present, we check the default capabilities of a key type (enc, sig or both)
  2. If a use property is present, we check if the value is valid for the key type (ed25519 can't have enc). If it's valid we only inlcude the verificadtionMethod types that link to sig vs enc. So if it's enc, only the keyAgreement will be added to the resolved did:jwk document

Comment on lines 4 to +9
Bls12381g1 = 'bls12381g1',
Bls12381g2 = 'bls12381g2',
X25519 = 'x25519',
P256 = 'p256',
P384 = 'p384',
P521 = 'p521',
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are the values here different from the spec (e.g. p256 vs P-256)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because we started with defining key types as lowercase in AFJ, and that's what we're now doing for consistency I guess 🤷

Copy link
Contributor

Choose a reason for hiding this comment

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

This is probably related to the discussion here

Copy link
Contributor

Choose a reason for hiding this comment

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

Because we started with defining key types as lowercase in AFJ, and that's what we're now doing for consistency I guess 🤷

If we aim for consistency, I'd say we follow the specs 😉. That way, people who are new to AFJ but are familiar with the specs will recognize things faster.

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 would say consistency is keeping aligned with what we're already doing, which is lowercase.

I would like to pick this up as part of the wallet refactoring, as there's different formats being used by Askar, JWK, etc... If we want to follow the JWK spec, it would be P-256, which would be inconsistent with what we have in AFJ (and we should then also change all other key types).

packages/core/src/modules/dids/methods/jwk/DidJwk.ts Outdated Show resolved Hide resolved
import { getJsonWebKey2020VerificationMethod } from '../verificationMethod'
import { VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, isJsonWebKey2020 } from '../verificationMethod/JsonWebKey2020'

export const keyDidJsonWebKey: KeyDidMapping = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not something for now, but I personally find these kinds of objects that contains various attributes and functions confusing. I know this is normal in the world of JS, but I don't think it's in line with the rest of the code base.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this particular case, I'm also not a fan of how it's implemented. But I do think it's quite a normal pattern to use, and don't see anything wrong with it. If you're only interested in static methods, an object that implements an interface is fine I think.

Would you prefer to refactor this to a class?

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with your point, but I just think it's very different from the rest of the code base. I would prefer refactoring to a class, purely out of consistency and readability reasons.

This is a personal preference, though. I'm not a fan of the JS way of things ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure we can refactor to a class. Just need to think about better naming for this paradigm

*/
function compressECPoint(x: Uint8Array, y: Uint8Array): Uint8Array {
const out = new Uint8Array(x.length + 1)
out[0] = 2 + (y[y.length - 1] & 1)
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah it seems like here it does the prefix, so it is already added. But there is no validate check so I am not sure what happend if the prefix is not 0x02 or 0x03.

export function expand(publicKey: Uint8Array, curve: 'P-256' | 'P-384' | 'P-521'): Uint8Array {
const publicKeyComponent = Buffer.from(publicKey).toString('hex')
const { prime, b, pIdent } = getConstantsForCurve(curve)
const signY = new Number(publicKeyComponent[1]).valueOf() - 2
Copy link
Contributor

Choose a reason for hiding this comment

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

Also happens here.

Copy link
Contributor

@berendsliedrecht berendsliedrecht left a comment

Choose a reason for hiding this comment

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

Great work @TimoGlastra! I see the prefixes of compressed and uncompressed are dealt with by code I did not see :). LGTM!

@TimoGlastra TimoGlastra enabled auto-merge (squash) May 10, 2023 19:35
Signed-off-by: Timo Glastra <timo@animo.id>
@TimoGlastra TimoGlastra merged commit 700d3f8 into openwallet-foundation:main May 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants