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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions packages/askar/src/utils/askarKeyTypes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import type { KeyType } from '@aries-framework/core'

import { KeyType } from '@aries-framework/core'
import { KeyAlgs } from '@hyperledger/aries-askar-shared'

export const keyTypeSupportedByAskar = (keyType: KeyType) =>
Object.entries(KeyAlgs).find(([, value]) => value === keyType.toString()) !== undefined
const keyTypeToAskarAlg = {
[KeyType.Ed25519]: KeyAlgs.Ed25519,
[KeyType.X25519]: KeyAlgs.X25519,
[KeyType.Bls12381g1]: KeyAlgs.Bls12381G1,
[KeyType.Bls12381g2]: KeyAlgs.Bls12381G2,
[KeyType.Bls12381g1g2]: KeyAlgs.Bls12381G1G2,
[KeyType.P256]: KeyAlgs.EcSecp256r1,
berendsliedrecht marked this conversation as resolved.
Show resolved Hide resolved
} as const

export const keyTypeSupportedByAskar = (keyType: KeyType) => keyType in keyTypeToAskarAlg
10 changes: 1 addition & 9 deletions packages/askar/src/wallet/AskarWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,7 @@ import {
WalletNotFoundError,
KeyDerivationMethod,
} from '@aries-framework/core'
import {
KdfMethod,
StoreKeyMethod,
KeyAlgs,
CryptoBox,
Store,
Key as AskarKey,
keyAlgFromString,
} from '@hyperledger/aries-askar-shared'
import { KeyAlgs, CryptoBox, Store, Key as AskarKey, keyAlgFromString } from '@hyperledger/aries-askar-shared'
// eslint-disable-next-line import/order
import BigNumber from 'bn.js'

Expand Down
14 changes: 10 additions & 4 deletions packages/askar/src/wallet/__tests__/AskarWallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ describeRunInNodeVersion([18], 'AskarWallet basic operations', () => {
})
})

test('Create P-256 keypair', async () => {
await expect(
askarWallet.createKey({ seed: Buffer.concat([seed, seed]), keyType: KeyType.P256 })
).resolves.toMatchObject({
keyType: KeyType.P256,
})
})

test('throws WalletKeyExistsError when a key already exists', async () => {
const privateKey = TypedArrayEncoder.fromString('2103de41b4ae37e8e28586d84a342b68')
await expect(askarWallet.createKey({ privateKey, keyType: KeyType.Ed25519 })).resolves.toEqual(expect.any(Key))
Expand All @@ -108,10 +116,8 @@ describeRunInNodeVersion([18], 'AskarWallet basic operations', () => {
)
})

describe.skip('Currently, all KeyTypes are supported by Askar natively', () => {
test('Fail to create a Bls12381g1g2 keypair', async () => {
await expect(askarWallet.createKey({ seed, keyType: KeyType.Bls12381g1g2 })).rejects.toThrowError(WalletError)
})
test('Fail to create a P384 keypair', async () => {
await expect(askarWallet.createKey({ seed, keyType: KeyType.P384 })).rejects.toThrowError(WalletError)
})

test('Create a signature with a ed25519 keypair', async () => {
Expand Down
33 changes: 20 additions & 13 deletions packages/cheqd/src/dids/CheqdDidRegistrar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type {
DidCreateResult,
DidDeactivateResult,
DidUpdateResult,
VerificationMethod,
} from '@aries-framework/core'
import type { CheqdNetwork, DIDDocument, DidStdFee, TVerificationKey, VerificationMethods } from '@cheqd/sdk'
import type { SignInfo } from '@cheqd/ts-proto/cheqd/did/v2'
Expand All @@ -22,6 +21,7 @@ import {
TypedArrayEncoder,
getKeyFromVerificationMethod,
JsonTransformer,
VerificationMethod,
} from '@aries-framework/core'
import { MethodSpecificIdAlgo, createDidVerificationMethod } from '@cheqd/sdk'
import { MsgCreateResourcePayload } from '@cheqd/ts-proto/cheqd/resource/v2'
Expand Down Expand Up @@ -182,16 +182,19 @@ export class CheqdDidRegistrar implements DidRegistrar {
})

didDocument.verificationMethod?.concat(
createDidVerificationMethod(
[verificationMethod.type as VerificationMethods],
[
{
methodSpecificId: didDocument.id.split(':')[3],
didUrl: didDocument.id,
keyId: `${didDocument.id}#${verificationMethod.id}`,
publicKey: TypedArrayEncoder.toHex(key.publicKey),
},
]
JsonTransformer.fromJSON(
createDidVerificationMethod(
[verificationMethod.type as VerificationMethods],
[
{
methodSpecificId: didDocument.id.split(':')[3],
didUrl: didDocument.id,
keyId: `${didDocument.id}#${verificationMethod.id}`,
publicKey: TypedArrayEncoder.toHex(key.publicKey),
},
]
),
VerificationMethod
)
)
}
Expand Down Expand Up @@ -253,6 +256,7 @@ export class CheqdDidRegistrar implements DidRegistrar {

try {
const { didDocument, didDocumentMetadata } = await cheqdLedgerService.resolve(did)

const didRecord = await didRepository.findCreatedDid(agentContext, did)
if (!didDocument || didDocumentMetadata.deactivated || !didRecord) {
return {
Expand All @@ -265,7 +269,8 @@ export class CheqdDidRegistrar implements DidRegistrar {
}
}
const payloadToSign = createMsgDeactivateDidDocPayloadToSign(didDocument, versionId)
const signInputs = await this.signPayload(agentContext, payloadToSign, didDocument.verificationMethod)
const didDocumentInstance = JsonTransformer.fromJSON(didDocument, DidDocument)
const signInputs = await this.signPayload(agentContext, payloadToSign, didDocumentInstance.verificationMethod)
const response = await cheqdLedgerService.deactivate(didDocument, signInputs, versionId)
if (response.code !== 0) {
throw new Error(`${response.rawLog}`)
Expand Down Expand Up @@ -332,7 +337,9 @@ export class CheqdDidRegistrar implements DidRegistrar {
data,
})
const payloadToSign = MsgCreateResourcePayload.encode(resourcePayload).finish()
const signInputs = await this.signPayload(agentContext, payloadToSign, didDocument.verificationMethod)

const didDocumentInstance = JsonTransformer.fromJSON(didDocument, DidDocument)
const signInputs = await this.signPayload(agentContext, payloadToSign, didDocumentInstance.verificationMethod)
const response = await cheqdLedgerService.createResource(did, resourcePayload, signInputs)
if (response.code !== 0) {
throw new Error(`${response.rawLog}`)
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"node-fetch": "^2.6.1",
"@types/ws": "^8.5.4",
"abort-controller": "^3.0.0",
"big-integer": "^1.6.51",
"borc": "^3.0.0",
"buffer": "^6.0.3",
"class-transformer": "0.5.1",
Expand All @@ -51,7 +52,6 @@
"web-did-resolver": "^2.0.21"
},
"devDependencies": {
"@types/bn.js": "^5.1.0",
"@types/events": "^3.0.0",
"@types/luxon": "^3.2.0",
"@types/object-inspect": "^1.8.0",
Expand Down
101 changes: 101 additions & 0 deletions packages/core/src/crypto/EcCompression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved
* Based on https://github.com/transmute-industries/verifiable-data/blob/main/packages/web-crypto-key-pair/src/compression/ec-compression.ts
*/

// native BigInteger is only supported in React Native 0.70+, so we use big-integer for now.
import bigInt from 'big-integer'

import { Buffer } from '../utils/buffer'

const curveToPointLength = {
'P-256': 64,
'P-384': 96,
'P-521': 132,
}

function getConstantsForCurve(curve: 'P-256' | 'P-384' | 'P-521') {
let two, prime, b, pIdent

if (curve === 'P-256') {
two = bigInt(2)
prime = two.pow(256).subtract(two.pow(224)).add(two.pow(192)).add(two.pow(96)).subtract(1)

pIdent = prime.add(1).divide(4)

b = bigInt('5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b', 16)
}

if (curve === 'P-384') {
two = bigInt(2)
prime = two.pow(384).subtract(two.pow(128)).subtract(two.pow(96)).add(two.pow(32)).subtract(1)

pIdent = prime.add(1).divide(4)
b = bigInt('b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef', 16)
}

if (curve === 'P-521') {
two = bigInt(2)
prime = two.pow(521).subtract(1)
b = bigInt(
'00000051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00',
16
)
pIdent = prime.add(1).divide(4)
}

if (!prime || !b || !pIdent) {
throw new Error(`Unsupported curve ${curve}`)
}

return { prime, b, pIdent }
}

// see https://stackoverflow.com/questions/17171542/algorithm-for-elliptic-curve-point-compression
// https://github.com/w3c-ccg/did-method-key/pull/36
/**
* Point compress elliptic curve key
* @return Compressed representation
*/
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.

out.set(x, 1)
return out
}

function padWithZeroes(number: number | string, length: number) {
let value = '' + number
while (value.length < length) {
value = '0' + value
}
return value
}

export function compress(publicKey: Uint8Array): Uint8Array {
const publicKeyHex = Buffer.from(publicKey).toString('hex')
const xHex = publicKeyHex.slice(0, publicKeyHex.length / 2)
const yHex = publicKeyHex.slice(publicKeyHex.length / 2, publicKeyHex.length)
const xOctet = Uint8Array.from(Buffer.from(xHex, 'hex'))
const yOctet = Uint8Array.from(Buffer.from(yHex, 'hex'))
return compressECPoint(xOctet, yOctet)
}

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.

const x = bigInt(publicKeyComponent.substring(2), 16)
// y^2 = x^3 - 3x + b
let y = x.pow(3).subtract(x.multiply(3)).add(b).modPow(pIdent, prime)

// If the parity doesn't match it's the *other* root
if (y.mod(2).toJSNumber() !== signY) {
// y = prime - y
y = prime.subtract(y)
}

return Buffer.from(
padWithZeroes(x.toString(16), curveToPointLength[curve]) + padWithZeroes(y.toString(16), curveToPointLength[curve]),
'hex'
)
}
73 changes: 73 additions & 0 deletions packages/core/src/crypto/Jwk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type {
Ed25519JwkPublicKey,
Jwk,
P256JwkPublicKey,
P384JwkPublicKey,
P521JwkPublicKey,
X25519JwkPublicKey,
} from './JwkTypes'
import type { Key } from './Key'

import { TypedArrayEncoder, Buffer } from '../utils'

import { compress, expand } from './EcCompression'
import {
jwkCurveToKeyTypeMapping,
keyTypeToJwkCurveMapping,
isEd25519JwkPublicKey,
isX25519JwkPublicKey,
isP256JwkPublicKey,
isP384JwkPublicKey,
isP521JwkPublicKey,
} from './JwkTypes'
import { KeyType } from './KeyType'

export function getKeyDataFromJwk(jwk: Jwk): { keyType: KeyType; publicKey: Uint8Array } {
// ed25519, x25519
if (isEd25519JwkPublicKey(jwk) || isX25519JwkPublicKey(jwk)) {
return {
publicKey: TypedArrayEncoder.fromBase64(jwk.x),
keyType: jwkCurveToKeyTypeMapping[jwk.crv],
}
}

// p-256, p-384, p-521
if (isP256JwkPublicKey(jwk) || isP384JwkPublicKey(jwk) || isP521JwkPublicKey(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?

const compressedPublicKey = compress(publicKeyBuffer)

return {
publicKey: compressedPublicKey,
keyType: jwkCurveToKeyTypeMapping[jwk.crv],
}
}

throw new Error(`Unsupported JWK kty '${jwk.kty}' and crv '${jwk.crv}'`)
}

export function getJwkFromKey(key: Key): Jwk {
if (key.keyType === KeyType.Ed25519 || key.keyType === KeyType.X25519) {
return {
kty: 'OKP',
crv: keyTypeToJwkCurveMapping[key.keyType],
x: TypedArrayEncoder.toBase64URL(key.publicKey),
} satisfies Ed25519JwkPublicKey | X25519JwkPublicKey
}

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.

const crv = keyTypeToJwkCurveMapping[key.keyType]
const expanded = expand(key.publicKey, crv)
const x = expanded.slice(0, expanded.length / 2)
const y = expanded.slice(expanded.length / 2)

return {
kty: 'EC',
crv,
x: TypedArrayEncoder.toBase64URL(x),
y: TypedArrayEncoder.toBase64URL(y),
} satisfies P256JwkPublicKey | P384JwkPublicKey | P521JwkPublicKey
}

throw new Error(`Cannot encode Key as JWK. Unsupported key type '${key.keyType}'`)
}
Loading