Skip to content

Commit

Permalink
feat: support for did:jwk and p-256, p-384, p-512 (#1446)
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <timo@animo.id>
  • Loading branch information
TimoGlastra committed May 11, 2023
1 parent 7dd4061 commit 700d3f8
Show file tree
Hide file tree
Showing 41 changed files with 1,337 additions and 73 deletions.
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,
} 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 @@
/**
* 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)
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
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)])
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) {
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

0 comments on commit 700d3f8

Please sign in to comment.