Skip to content

Commit

Permalink
feat: Add support for ES256/Secp256r1 DID JWKs
Browse files Browse the repository at this point in the history
  • Loading branch information
nklomp committed Oct 27, 2022
1 parent 5254adf commit 1e447a6
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 36 deletions.
136 changes: 122 additions & 14 deletions packages/jwk-did-provider/__tests__/comparison-regression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createAgent, DIDResolutionResult, IIdentifier, IKeyManager } from '@ver
import { DIDManager, MemoryDIDStore } from '@veramo/did-manager'
import { KeyManager, MemoryKeyStore, MemoryPrivateKeyStore } from '@veramo/key-manager'
import { KeyManagementSystem } from '@veramo/kms-local'
import { getDidJwkResolver, JwkDIDProvider } from '../src'
import { getDidJwkResolver, IKeyOpts, JwkDIDProvider, Key, KeyUse } from '../src'
import { DIDResolverPlugin } from '@veramo/did-resolver'
import { Resolver } from 'did-resolver'
import base64url from 'base64url'
Expand Down Expand Up @@ -39,7 +39,101 @@ const agent = createAgent<IKeyManager, DIDManager>({
],
})

describe('@sphereon/jwk-did-provider comparison', () => {
describe('@sphereon/jwk-did-provider comparison ES256k', () => {
it('external JWK should result in equal DID Document', async () => {
// const client = method.create({});
const { privateKeyJwk, publicKeyJwk } = await method.generateKeyPair('ES256K')

console.log(JSON.stringify(privateKeyJwk, null, 2))
console.log(JSON.stringify(publicKeyJwk, null, 2))
const did = await method.toDid(publicKeyJwk)
console.log(did)

const didResolutionResult: DIDResolutionResult = await agent.resolveDid({ didUrl: did })

// We append the relationships, since the other lib does not take into account the use
const comparisonDidDoc = await method.toDidDocument(publicKeyJwk)
console.log(JSON.stringify(comparisonDidDoc, null, 2))
console.log(JSON.stringify(didResolutionResult, null, 2))
// We add the relationships to our
expect(didResolutionResult.didDocument).toEqual(comparisonDidDoc)
})

it('test resolution', async () => {
const jwk = {
kid: 'urn:ietf:params:oauth:jwk-thumbprint:sha-256:VmU0yXoR5Jgaoz6SDzt-awKL22Gbfoo2Wvk-cayLxlI',
kty: 'EC',
crv: 'secp256k1',
alg: 'ES256K',
x: 'lglTr5VNye5utTm0wKFpzduHFfuZOQmZU8xzvGmP0vU',
y: 'onYhSokMMFxsDcyqhlAx9scMuoa19TRv2gFUyKhMlRI',
// d: "9-CUAh2TXjCmjp5WVBdwHny3liSIEmwa2zZdFonq_Yw"
}

const publicKeyHex = `04${base64url.decode(jwk.x, 'hex')}${base64url.decode(jwk.y, 'hex')}`
console.log(publicKeyHex)
const did = `did:jwk:${base64url.encode(JSON.stringify(jwk))}`
console.log(did)

// Resolution
const comparisonDidDoc = await method.toDidDocument(jwk)

console.log(JSON.stringify(comparisonDidDoc, null, 2))
console.log('-----------------')
console.log(JSON.stringify(await method.resolve(did)))

const didResolutionResult: DIDResolutionResult = await agent.resolveDid({ didUrl: did })

console.log(JSON.stringify(didResolutionResult.didDocument, null, 2))
expect(didResolutionResult.didDocument).toEqual(comparisonDidDoc)
})

it('Creation from privateKeyHex', async () => {
const privateKeyHex = 'e8fa0da4d6e7dcdf77b70e4fb0e304bb7cbcb3aeddf33257f0e007a602a46d42'
const options: IKeyOpts = {
key: {
privateKeyHex,
},
use: KeyUse.Signature,
}
const identifier: IIdentifier = await agent.didManagerCreate({ options })

const did =
'did:jwk:eyJ1c2UiOiJzaWciLCJrdHkiOiJFQyIsImNydiI6InNlY3AyNTZrMSIsIngiOiJmYjY5SEE2M244ZENKd0RmaVJONGxacUtVVU1odHYyZE5BemdjUjJNY0ZBIiwieSI6Ikd3amFWNHpuSm1EZDBOdFlSWGdJeW5aOFlyWDRqN0lzLXFselFuekppclEifQ'
expect(identifier.did).toBe(did)

console.log(JSON.stringify(await method.resolve(did), null, 2))
const didResolutionResult: DIDResolutionResult = await agent.resolveDid({ didUrl: did })
console.log(JSON.stringify(didResolutionResult.didDocument, null, 2))

const jwk = {
kty: 'EC',
use: 'sig',
crv: 'secp256k1',
x: 'fb69HA63n8dCJwDfiRN4lZqKUUMhtv2dNAzgcR2McFA',
y: 'GwjaV4znJmDd0NtYRXgIynZ8YrX4j7Is-qlzQnzJirQ',
}
const verificationMethod = {
controller:
'did:jwk:eyJ1c2UiOiJzaWciLCJrdHkiOiJFQyIsImNydiI6InNlY3AyNTZrMSIsIngiOiJmYjY5SEE2M244ZENKd0RmaVJONGxacUtVVU1odHYyZE5BemdjUjJNY0ZBIiwieSI6Ikd3amFWNHpuSm1EZDBOdFlSWGdJeW5aOFlyWDRqN0lzLXFselFuekppclEifQ',
id: '#0',
publicKeyJwk: jwk,
type: 'JsonWebKey2020',
}

expect(didResolutionResult!.didDocument!.verificationMethod).toEqual([verificationMethod])
// We correctly resolve the use property. The other lib does not, so let's add it to their response
expect(didResolutionResult!.didDocument).toEqual({
assertionMethod: ['#0'],
authentication: ['#0'],
capabilityDelegation: ['#0'],
capabilityInvocation: ['#0'],
...(await method.resolve(did)),
})
})
})

describe('@sphereon/jwk-did-provider comparison ES256', () => {
it('external JWK should result in equal DID Document', async () => {
// const client = method.create({});
const { privateKeyJwk, publicKeyJwk } = await method.generateKeyPair('ES256')
Expand All @@ -51,9 +145,11 @@ describe('@sphereon/jwk-did-provider comparison', () => {

const didResolutionResult: DIDResolutionResult = await agent.resolveDid({ didUrl: did })

// We append the relationships, since the other lib does not take into account the use
const comparisonDidDoc = await method.toDidDocument(publicKeyJwk)
console.log(JSON.stringify(comparisonDidDoc, null, 2))
console.log(JSON.stringify(didResolutionResult, null, 2))
// We add the relationships to our
expect(didResolutionResult.didDocument).toEqual(comparisonDidDoc)
})

Expand All @@ -80,42 +176,54 @@ describe('@sphereon/jwk-did-provider comparison', () => {
console.log('-----------------')
console.log(JSON.stringify(await method.resolve(did)))

const didResolutionResult: DIDResolutionResult = await agent.resolveDid({ didUrl: did})
const didResolutionResult: DIDResolutionResult = await agent.resolveDid({ didUrl: did })

console.log(JSON.stringify(didResolutionResult.didDocument, null, 2))
expect(didResolutionResult.didDocument).toEqual(comparisonDidDoc)
})

it('Creation from privateKeyHex', async () => {
const privateKeyHex = 'e8fa0da4d6e7dcdf77b70e4fb0e304bb7cbcb3aeddf33257f0e007a602a46d42'
const options = {
const options: IKeyOpts = {
key: {
privateKeyHex,
},
use: KeyUse.Signature,
type: Key.Secp256r1,
}
const identifier: IIdentifier = await agent.didManagerCreate({ options })

const did = 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6InNlY3AyNTZrMSIsIngiOiJmYjY5SEE2M244ZENKd0RmaVJONGxacUtVVU1odHYyZE5BemdjUjJNY0ZBIiwieSI6Ikd3amFWNHpuSm1EZDBOdFlSWGdJeW5aOFlyWDRqN0lzLXFselFuekppclEifQ'
const did =
'did:jwk:eyJ1c2UiOiJzaWciLCJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IktQalQxY0IwYU1XclBzVGp3cmdtMEhwSVNwUHZ6aGpyVGxfakVLQVhrUSIsInkiOiJpeVlGZnRwZXl5dk9FTUtjR01pOFpvT3BjVy1ULU4yc2szUl9FaVZYQmdzIn0'
expect(identifier.did).toBe(did)

console.log(JSON.stringify(await method.resolve(did), null, 2))
const didResolutionResult: DIDResolutionResult = await agent.resolveDid({ didUrl: did})
const didResolutionResult: DIDResolutionResult = await agent.resolveDid({ didUrl: did })
console.log(JSON.stringify(didResolutionResult.didDocument, null, 2))

const jwk = {
"kty": "EC",
"crv": "secp256k1",
"x": "fb69HA63n8dCJwDfiRN4lZqKUUMhtv2dNAzgcR2McFA",
"y": "GwjaV4znJmDd0NtYRXgIynZ8YrX4j7Is-qlzQnzJirQ"
kty: 'EC',
use: 'sig',
crv: 'P-256',
x: 'KPjT1cB0aMWrPsTjwrgm0HpISpPvzhjrTl_jEKAXkQ',
y: 'iyYFftpeyyvOEMKcGMi8ZoOpcW-T-N2sk3R_EiVXBgs',
}
const verificationMethod = {
controller: "did:jwk:eyJrdHkiOiJFQyIsImNydiI6InNlY3AyNTZrMSIsIngiOiJmYjY5SEE2M244ZENKd0RmaVJONGxacUtVVU1odHYyZE5BemdjUjJNY0ZBIiwieSI6Ikd3amFWNHpuSm1EZDBOdFlSWGdJeW5aOFlyWDRqN0lzLXFselFuekppclEifQ",
id: "#0",
controller:
'did:jwk:eyJ1c2UiOiJzaWciLCJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IktQalQxY0IwYU1XclBzVGp3cmdtMEhwSVNwUHZ6aGpyVGxfakVLQVhrUSIsInkiOiJpeVlGZnRwZXl5dk9FTUtjR01pOFpvT3BjVy1ULU4yc2szUl9FaVZYQmdzIn0',
id: '#0',
publicKeyJwk: jwk,
type: "JsonWebKey2020"
type: 'JsonWebKey2020',
}

expect(didResolutionResult!.didDocument!.verificationMethod).toEqual([verificationMethod])
expect(didResolutionResult!.didDocument).toEqual(await method.resolve(did))
// We correctly resolve the use property. The other lib does not, so let's add it to their response
expect(didResolutionResult!.didDocument).toEqual({
assertionMethod: ['#0'],
authentication: ['#0'],
capabilityDelegation: ['#0'],
capabilityInvocation: ['#0'],
...(await method.resolve(did)),
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('@sphereon/jwk-did-resolver', () => {
ContextType.DidDocument,
{
'@vocab': VocabType.Jose,
}
},
])
expect(didResolutionResult.didDocument!.id).toEqual(did)
expect(didResolutionResult.didDocument!.verificationMethod).toBeDefined()
Expand Down
1 change: 1 addition & 0 deletions packages/jwk-did-provider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@stablelib/ed25519": "^1.0.2",
"@veramo/core": "4.0.2-next.19",
"@veramo/did-manager": "4.0.2-next.19",
"elliptic": "^6.5.4",
"base64url": "^3.0.1",
"debug": "^4.1.1"
},
Expand Down
1 change: 1 addition & 0 deletions packages/jwk-did-provider/src/elliptic.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module 'elliptic'
45 changes: 39 additions & 6 deletions packages/jwk-did-provider/src/functions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { randomBytes } from '@ethersproject/random'
import { generateKeyPair as generateSigningKeyPair } from '@stablelib/ed25519'
import { TKeyType } from '@veramo/core'
import { JsonWebKey } from 'did-resolver'
import * as u8a from 'uint8arrays'
import { Key, KeyCurve, KeyType, KeyUse } from './types/jwk-provider-types'
import { ENC_KEY_ALGS, Key, KeyCurve, KeyType, KeyUse, SIG_KEY_ALGS } from './types/jwk-provider-types'
import elliptic from 'elliptic'

/**
* Generates a random Private Hex Key for the specified key type
Expand Down Expand Up @@ -49,7 +51,9 @@ export const generateJwk = (publicKeyHex: string, type: Key, use?: KeyUse): Json
case Key.Ed25519:
return generateEd25519Jwk(publicKeyHex, use)
case Key.Secp256k1:
return generateSecp256k1Jwk(publicKeyHex, use)
return generateSecp256Jwk(publicKeyHex, KeyCurve.Secp256k1, use)
case Key.Secp256r1:
return generateSecp256r1Jwk(publicKeyHex, use)
default:
throw new Error('Key type not supported')
}
Expand All @@ -58,19 +62,44 @@ export const generateJwk = (publicKeyHex: string, type: Key, use?: KeyUse): Json
/**
* Generates a JWK from a Secp256k1 public key
* @param publicKeyHex Secp256k1 public key in hex
* @param keyCurve The key curve to use Secp256k1 or P-256
* @param use The use for the key
* @return The JWK
*/
const generateSecp256k1Jwk = (publicKeyHex: string, use?: KeyUse): JsonWebKey => {
const generateSecp256Jwk = (publicKeyHex: string, keyCurve: KeyCurve.Secp256k1 | KeyCurve.P_256, use?: KeyUse): JsonWebKey => {
return {
...(use && { use }),
...(use !== undefined && { use }),
kty: KeyType.EC,
crv: KeyCurve.Secp256k1,
crv: keyCurve,
x: hex2base64url(publicKeyHex.substr(2, 64)),
y: hex2base64url(publicKeyHex.substr(66, 64)),
}
}

/**
* Generates a JWK from a Secp256k1 public key
* @param publicKeyHex Secp256k1 public key in hex
* @param keyCurve The key curve to use Secp256k1 or P-256
* @param use The use for the key
* @return The JWK
*/
const generateSecp256r1Jwk = (publicKeyHex: string, use?: KeyUse): JsonWebKey => {
// const privateBytes = u8a.fromString(args.privateKeyHex.toLowerCase(), 'base16')
const secp256r1 = new elliptic.ec('p256')
const publicKey = `03${publicKeyHex}` // We add the 'compressed' type 03 prefix
const key = secp256r1.keyFromPublic(publicKey, 'hex')
var pubPoint = key.getPublic()
var x = pubPoint.getX()
var y = pubPoint.getY()
return {
...(use !== undefined && { use }),
kty: KeyType.EC,
crv: KeyCurve.P_256,
x: hex2base64url(x.toString('hex')),
y: hex2base64url(y.toString('hex')),
}
}

/**
* Generates a JWK from an Ed25519 public key
* @param publicKeyHex Ed25519 public key in hex
Expand All @@ -79,9 +108,13 @@ const generateSecp256k1Jwk = (publicKeyHex: string, use?: KeyUse): JsonWebKey =>
*/
const generateEd25519Jwk = (publicKeyHex: string, use?: KeyUse): JsonWebKey => {
return {
...(use && { use }),
...(use !== undefined && { use }),
kty: KeyType.OKP,
crv: KeyCurve.Ed25519,
x: hex2base64url(publicKeyHex.substr(0, 64)),
}
}

export const determineUse = (type: TKeyType, suppliedUse?: KeyUse): KeyUse | undefined => {
return suppliedUse ? suppliedUse : SIG_KEY_ALGS.includes(type) ? KeyUse.Signature : ENC_KEY_ALGS.includes(type) ? KeyUse.Encryption : undefined
}
7 changes: 5 additions & 2 deletions packages/jwk-did-provider/src/jwk-did-provider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DIDDocument, IAgentContext, IIdentifier, IKey, IKeyManager } from '@veramo/core'
import { AbstractIdentifierProvider } from '@veramo/did-manager'
import base64url from 'base64url'
import { generateJwk, generatePrivateKeyHex } from '../src/functions'
import { determineUse, generateJwk, generatePrivateKeyHex } from '../src/functions'
import {
IAddKeyArgs,
IAddServiceArgs,
Expand Down Expand Up @@ -37,8 +37,11 @@ export class JwkDIDProvider extends AbstractIdentifierProvider {
},
context
)

const use = determineUse(key.type, args?.options?.use)

// TODO test key type
const jwk: JsonWebKey = generateJwk(key.publicKeyHex, key.type as Key, args.options?.use)
const jwk: JsonWebKey = generateJwk(key.publicKeyHex, key.type as Key, use)
const identifier: Omit<IIdentifier, 'provider'> = {
did: `did:jwk:${base64url(JSON.stringify(jwk))}`,
controllerKeyId: '#0',
Expand Down
23 changes: 11 additions & 12 deletions packages/jwk-did-provider/src/jwk-did-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { IParsedDID, parseDid } from '@sphereon/ssi-types'
import base64url from 'base64url'
import { DIDResolutionOptions, DIDResolutionResult, DIDResolver, JsonWebKey } from 'did-resolver'
import {
ContextType,
KeyUse,
VerificationType,
VocabType
} from './types/jwk-provider-types'
import { ContextType, ENC_KEY_ALGS, KeyUse, SIG_KEY_ALGS, VerificationType, VocabType } from './types/jwk-provider-types'

export const resolveDidJwk: DIDResolver = async (didUrl: string, options?: DIDResolutionOptions): Promise<DIDResolutionResult> => {
return resolve(didUrl, options)
Expand All @@ -32,7 +27,11 @@ const resolve = async (didUrl: string, options?: DIDResolutionOptions): Promise<
}

// We need this since DIDResolutionResult does not allow for an object in the array
const context = [ ContextType.DidDocument, { '@vocab': VocabType.Jose } ] as never
const context = [ContextType.DidDocument, { '@vocab': VocabType.Jose }] as never

// We add the alg check to ensure max compatibility with implementations that do not export the use property
const enc = (jwk.use && jwk.use === KeyUse.Encryption) || (jwk.alg && ENC_KEY_ALGS.includes(jwk.alg))
const sig = (jwk.use && jwk.use === KeyUse.Signature) || (jwk.alg && SIG_KEY_ALGS.includes(jwk.alg))

const didResolution: DIDResolutionResult = {
didResolutionMetadata: {
Expand All @@ -56,11 +55,11 @@ const resolve = async (didUrl: string, options?: DIDResolutionOptions): Promise<
publicKeyJwk: jwk,
},
],
...((jwk.use && jwk.use !== KeyUse.Encryption || jwk.alg) && { assertionMethod: ['#0'] }),
...((jwk.use && jwk.use !== KeyUse.Encryption || jwk.alg) && { authentication: ['#0'] }),
...((jwk.use && jwk.use !== KeyUse.Encryption || jwk.alg) && { capabilityInvocation: ['#0'] }),
...((jwk.use && jwk.use !== KeyUse.Encryption || jwk.alg) && { capabilityDelegation: ['#0'] }),
...(jwk.use && jwk.use === KeyUse.Encryption && { keyAgreement: ['#0'] }),
...(sig && { assertionMethod: ['#0'] }),
...(sig && { authentication: ['#0'] }),
...(sig && { capabilityInvocation: ['#0'] }),
...(sig && { capabilityDelegation: ['#0'] }),
...(enc && { keyAgreement: ['#0'] }),
},
didDocumentMetadata: {},
}
Expand Down
Loading

0 comments on commit 1e447a6

Please sign in to comment.