diff --git a/packages/askar/src/utils/askarKeyTypes.ts b/packages/askar/src/utils/askarKeyTypes.ts index bb837f962e..5d59a26493 100644 --- a/packages/askar/src/utils/askarKeyTypes.ts +++ b/packages/askar/src/utils/askarKeyTypes.ts @@ -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 diff --git a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts index 43ec06759c..446ec79cae 100644 --- a/packages/askar/src/wallet/__tests__/AskarWallet.test.ts +++ b/packages/askar/src/wallet/__tests__/AskarWallet.test.ts @@ -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)) @@ -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 () => { diff --git a/packages/cheqd/src/dids/CheqdDidRegistrar.ts b/packages/cheqd/src/dids/CheqdDidRegistrar.ts index a23ecf1456..0f4c243098 100644 --- a/packages/cheqd/src/dids/CheqdDidRegistrar.ts +++ b/packages/cheqd/src/dids/CheqdDidRegistrar.ts @@ -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' @@ -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' @@ -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 ) ) } @@ -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 { @@ -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}`) @@ -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}`) diff --git a/packages/core/package.json b/packages/core/package.json index be6bdb7498..9eb5d56a3f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", @@ -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", diff --git a/packages/core/src/crypto/EcCompression.ts b/packages/core/src/crypto/EcCompression.ts new file mode 100644 index 0000000000..42cbd30398 --- /dev/null +++ b/packages/core/src/crypto/EcCompression.ts @@ -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' + ) +} diff --git a/packages/core/src/crypto/Jwk.ts b/packages/core/src/crypto/Jwk.ts new file mode 100644 index 0000000000..2c8d49d243 --- /dev/null +++ b/packages/core/src/crypto/Jwk.ts @@ -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}'`) +} diff --git a/packages/core/src/crypto/JwkTypes.ts b/packages/core/src/crypto/JwkTypes.ts index 144e771f16..560b2c5e1d 100644 --- a/packages/core/src/crypto/JwkTypes.ts +++ b/packages/core/src/crypto/JwkTypes.ts @@ -1,6 +1,139 @@ +import { KeyType } from './KeyType' + +export type JwkCurve = 'Ed25519' | 'X25519' | 'P-256' | 'P-384' | 'P-521' | 'Bls12381G1' | 'Bls12381G2' + export interface Jwk { kty: 'EC' | 'OKP' - crv: 'Ed25519' | 'X25519' | 'P-256' | 'P-384' | 'secp256k1' + crv: JwkCurve x: string y?: string + use?: 'sig' | 'enc' +} + +export interface Ed25519JwkPublicKey extends Jwk { + kty: 'OKP' + crv: 'Ed25519' + x: string + y?: never + use?: 'sig' +} + +export interface X25519JwkPublicKey extends Jwk { + kty: 'OKP' + crv: 'X25519' + x: string + y?: never + use?: 'enc' +} + +export interface P256JwkPublicKey extends Jwk { + kty: 'EC' + crv: 'P-256' + x: string + y: string + use?: 'sig' | 'enc' +} + +export interface P384JwkPublicKey extends Jwk { + kty: 'EC' + crv: 'P-384' + x: string + y: string + use?: 'sig' | 'enc' +} + +export interface P521JwkPublicKey extends Jwk { + kty: 'EC' + crv: 'P-521' + x: string + y: string + use?: 'sig' | 'enc' +} + +export function isEd25519JwkPublicKey(jwk: Jwk): jwk is Ed25519JwkPublicKey { + return jwk.kty === 'OKP' && jwk.crv === 'Ed25519' && jwk.x !== undefined && (!jwk.use || jwk.use === 'sig') +} + +export function isX25519JwkPublicKey(jwk: Jwk): jwk is X25519JwkPublicKey { + return jwk.kty === 'OKP' && jwk.crv === 'X25519' && jwk.x !== undefined && (!jwk.use || jwk.use === 'enc') +} + +export function isP256JwkPublicKey(jwk: Jwk): jwk is P256JwkPublicKey { + return ( + jwk.kty === 'EC' && + jwk.crv === 'P-256' && + jwk.x !== undefined && + jwk.y !== undefined && + (!jwk.use || ['sig', 'enc'].includes(jwk.use)) + ) +} + +export function isP384JwkPublicKey(jwk: Jwk): jwk is P384JwkPublicKey { + return ( + jwk.kty === 'EC' && + jwk.crv === 'P-384' && + jwk.x !== undefined && + jwk.y !== undefined && + (!jwk.use || ['sig', 'enc'].includes(jwk.use)) + ) +} + +export function isP521JwkPublicKey(jwk: Jwk): jwk is P521JwkPublicKey { + return ( + jwk.kty === 'EC' && + jwk.crv === 'P-521' && + jwk.x !== undefined && + jwk.y !== undefined && + (!jwk.use || ['sig', 'enc'].includes(jwk.use)) + ) +} + +export const jwkCurveToKeyTypeMapping = { + Ed25519: KeyType.Ed25519, + X25519: KeyType.X25519, + 'P-256': KeyType.P256, + 'P-384': KeyType.P384, + 'P-521': KeyType.P521, + Bls12381G1: KeyType.Bls12381g1, + Bls12381G2: KeyType.Bls12381g2, +} as const + +export const keyTypeToJwkCurveMapping = { + [KeyType.Ed25519]: 'Ed25519', + [KeyType.X25519]: 'X25519', + [KeyType.P256]: 'P-256', + [KeyType.P384]: 'P-384', + [KeyType.P521]: 'P-521', + [KeyType.Bls12381g1]: 'Bls12381G1', + [KeyType.Bls12381g2]: 'Bls12381G2', +} as const + +const keyTypeSigningSupportedMapping = { + [KeyType.Ed25519]: true, + [KeyType.X25519]: false, + [KeyType.P256]: true, + [KeyType.P384]: true, + [KeyType.P521]: true, + [KeyType.Bls12381g1]: true, + [KeyType.Bls12381g2]: true, + [KeyType.Bls12381g1g2]: true, +} as const + +const keyTypeEncryptionSupportedMapping = { + [KeyType.Ed25519]: false, + [KeyType.X25519]: true, + [KeyType.P256]: true, + [KeyType.P384]: true, + [KeyType.P521]: true, + [KeyType.Bls12381g1]: false, + [KeyType.Bls12381g2]: false, + [KeyType.Bls12381g1g2]: false, +} as const + +export function isSigningSupportedForKeyType(keyType: KeyType): boolean { + return keyTypeSigningSupportedMapping[keyType] +} + +export function isEncryptionSupportedForKeyType(keyType: KeyType): boolean { + return keyTypeEncryptionSupportedMapping[keyType] } diff --git a/packages/core/src/crypto/Key.ts b/packages/core/src/crypto/Key.ts index 8c4a7c6204..61efb67ef3 100644 --- a/packages/core/src/crypto/Key.ts +++ b/packages/core/src/crypto/Key.ts @@ -1,10 +1,11 @@ import type { Jwk } from './JwkTypes' +import type { KeyType } from './KeyType' -import { AriesFrameworkError } from '../error' import { Buffer, MultiBaseEncoder, TypedArrayEncoder, VarintEncoder } from '../utils' -import { KeyType } from './KeyType' -import { getKeyTypeByMultiCodecPrefix, getMultiCodecPrefixByKeytype } from './multiCodecKey' +import { getJwkFromKey, getKeyDataFromJwk } from './Jwk' +import { isEncryptionSupportedForKeyType, isSigningSupportedForKeyType } from './JwkTypes' +import { getKeyTypeByMultiCodecPrefix, getMultiCodecPrefixByKeyType } from './multiCodecKey' export class Key { public readonly publicKey: Buffer @@ -41,7 +42,7 @@ export class Key { } public get prefixedPublicKey() { - const multiCodecPrefix = getMultiCodecPrefixByKeytype(this.keyType) + const multiCodecPrefix = getMultiCodecPrefixByKeyType(this.keyType) // Create Buffer with length of the prefix bytes, then use varint to fill the prefix bytes const prefixBytes = VarintEncoder.encode(multiCodecPrefix) @@ -58,22 +59,21 @@ export class Key { return TypedArrayEncoder.toBase58(this.publicKey) } + public get supportsEncrypting() { + return isEncryptionSupportedForKeyType(this.keyType) + } + + public get supportsSigning() { + return isSigningSupportedForKeyType(this.keyType) + } + public toJwk(): Jwk { - if (this.keyType !== KeyType.Ed25519) { - throw new AriesFrameworkError(`JWK creation is only supported for Ed25519 key types. Received ${this.keyType}`) - } - - return { - kty: 'OKP', - crv: 'Ed25519', - x: TypedArrayEncoder.toBase64URL(this.publicKey), - } + return getJwkFromKey(this) } public static fromJwk(jwk: Jwk) { - if (jwk.crv !== 'Ed25519') { - throw new AriesFrameworkError('Only JWKs with Ed25519 key type is supported.') - } - return Key.fromPublicKeyBase58(TypedArrayEncoder.toBase58(TypedArrayEncoder.fromBase64(jwk.x)), KeyType.Ed25519) + const { keyType, publicKey } = getKeyDataFromJwk(jwk) + + return Key.fromPublicKey(publicKey, keyType) } } diff --git a/packages/core/src/crypto/KeyType.ts b/packages/core/src/crypto/KeyType.ts index 858762f670..d378e4bffb 100644 --- a/packages/core/src/crypto/KeyType.ts +++ b/packages/core/src/crypto/KeyType.ts @@ -4,4 +4,7 @@ export enum KeyType { Bls12381g1 = 'bls12381g1', Bls12381g2 = 'bls12381g2', X25519 = 'x25519', + P256 = 'p256', + P384 = 'p384', + P521 = 'p521', } diff --git a/packages/core/src/crypto/__tests__/Jwk.test.ts b/packages/core/src/crypto/__tests__/Jwk.test.ts new file mode 100644 index 0000000000..0cbe81255e --- /dev/null +++ b/packages/core/src/crypto/__tests__/Jwk.test.ts @@ -0,0 +1,97 @@ +import type { + Ed25519JwkPublicKey, + P256JwkPublicKey, + P384JwkPublicKey, + P521JwkPublicKey, + X25519JwkPublicKey, +} from '../JwkTypes' + +import { getJwkFromKey, getKeyDataFromJwk } from '../Jwk' +import { Key } from '../Key' +import { KeyType } from '../KeyType' + +describe('jwk', () => { + it('Ed25519', () => { + const fingerprint = 'z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp' + const jwk = { + kty: 'OKP', + crv: 'Ed25519', + x: 'O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik', + } satisfies Ed25519JwkPublicKey + + const { keyType, publicKey } = getKeyDataFromJwk(jwk) + expect(keyType).toEqual(KeyType.Ed25519) + expect(Key.fromPublicKey(publicKey, KeyType.Ed25519).fingerprint).toEqual(fingerprint) + + const actualJwk = getJwkFromKey(Key.fromFingerprint(fingerprint)) + expect(actualJwk).toEqual(jwk) + }) + + it('X25519', () => { + const fingerprint = 'z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW' + const jwk = { + kty: 'OKP', + crv: 'X25519', + x: 'W_Vcc7guviK-gPNDBmevVw-uJVamQV5rMNQGUwCqlH0', + } satisfies X25519JwkPublicKey + + const { keyType, publicKey } = getKeyDataFromJwk(jwk) + expect(keyType).toEqual(KeyType.X25519) + expect(Key.fromPublicKey(publicKey, KeyType.X25519).fingerprint).toEqual(fingerprint) + + const actualJwk = getJwkFromKey(Key.fromFingerprint(fingerprint)) + expect(actualJwk).toEqual(jwk) + }) + + it('P-256', () => { + const fingerprint = 'zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv' + const jwk = { + kty: 'EC', + crv: 'P-256', + x: 'igrFmi0whuihKnj9R3Om1SoMph72wUGeFaBbzG2vzns', + y: 'efsX5b10x8yjyrj4ny3pGfLcY7Xby1KzgqOdqnsrJIM', + } satisfies P256JwkPublicKey + + const { keyType, publicKey } = getKeyDataFromJwk(jwk) + expect(keyType).toEqual(KeyType.P256) + expect(Key.fromPublicKey(publicKey, KeyType.P256).fingerprint).toEqual(fingerprint) + + const actualJwk = getJwkFromKey(Key.fromFingerprint(fingerprint)) + expect(actualJwk).toEqual(jwk) + }) + + it('P-384', () => { + const fingerprint = 'z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9' + const jwk = { + kty: 'EC', + crv: 'P-384', + x: 'lInTxl8fjLKp_UCrxI0WDklahi-7-_6JbtiHjiRvMvhedhKVdHBfi2HCY8t_QJyc', + y: 'y6N1IC-2mXxHreETBW7K3mBcw0qGr3CWHCs-yl09yCQRLcyfGv7XhqAngHOu51Zv', + } satisfies P384JwkPublicKey + + const { keyType, publicKey } = getKeyDataFromJwk(jwk) + expect(keyType).toEqual(KeyType.P384) + expect(Key.fromPublicKey(publicKey, KeyType.P384).fingerprint).toEqual(fingerprint) + + const actualJwk = getJwkFromKey(Key.fromFingerprint(fingerprint)) + expect(actualJwk).toEqual(jwk) + }) + + it('P-521', () => { + const fingerprint = + 'z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7' + const jwk = { + kty: 'EC', + crv: 'P-521', + x: 'ASUHPMyichQ0QbHZ9ofNx_l4y7luncn5feKLo3OpJ2nSbZoC7mffolj5uy7s6KSKXFmnNWxGJ42IOrjZ47qqwqyS', + y: 'AW9ziIC4ZQQVSNmLlp59yYKrjRY0_VqO-GOIYQ9tYpPraBKUloEId6cI_vynCzlZWZtWpgOM3HPhYEgawQ703RjC', + } satisfies P521JwkPublicKey + + const { keyType, publicKey } = getKeyDataFromJwk(jwk) + expect(keyType).toEqual(KeyType.P521) + expect(Key.fromPublicKey(publicKey, KeyType.P521).fingerprint).toEqual(fingerprint) + + const actualJwk = getJwkFromKey(Key.fromFingerprint(fingerprint)) + expect(actualJwk).toEqual(jwk) + }) +}) diff --git a/packages/core/src/crypto/keyUtils.ts b/packages/core/src/crypto/keyUtils.ts index 9022d2095d..d772c63e92 100644 --- a/packages/core/src/crypto/keyUtils.ts +++ b/packages/core/src/crypto/keyUtils.ts @@ -9,6 +9,9 @@ export function isValidSeed(seed: Buffer, keyType: KeyType): boolean { [KeyType.Bls12381g1]: 32, [KeyType.Bls12381g2]: 32, [KeyType.Bls12381g1g2]: 32, + [KeyType.P256]: 64, + [KeyType.P384]: 64, + [KeyType.P521]: 64, } return Buffer.isBuffer(seed) && seed.length >= minimumSeedLength[keyType] @@ -21,6 +24,9 @@ export function isValidPrivateKey(privateKey: Buffer, keyType: KeyType): boolean [KeyType.Bls12381g1]: 32, [KeyType.Bls12381g2]: 32, [KeyType.Bls12381g1g2]: 32, + [KeyType.P256]: 32, + [KeyType.P384]: 48, + [KeyType.P521]: 66, } return Buffer.isBuffer(privateKey) && privateKey.length === privateKeyLength[keyType] diff --git a/packages/core/src/crypto/multiCodecKey.ts b/packages/core/src/crypto/multiCodecKey.ts index 20d3f4b070..1ebcbd5ca9 100644 --- a/packages/core/src/crypto/multiCodecKey.ts +++ b/packages/core/src/crypto/multiCodecKey.ts @@ -7,6 +7,9 @@ const multiCodecPrefixMap: Record = { 236: KeyType.X25519, 237: KeyType.Ed25519, 238: KeyType.Bls12381g1g2, + 4608: KeyType.P256, + 4609: KeyType.P384, + 4610: KeyType.P521, } export function getKeyTypeByMultiCodecPrefix(multiCodecPrefix: number): KeyType { @@ -19,7 +22,7 @@ export function getKeyTypeByMultiCodecPrefix(multiCodecPrefix: number): KeyType return keyType } -export function getMultiCodecPrefixByKeytype(keyType: KeyType): number { +export function getMultiCodecPrefixByKeyType(keyType: KeyType): number { const codes = Object.keys(multiCodecPrefixMap) const code = codes.find((key) => multiCodecPrefixMap[key] === keyType) diff --git a/packages/core/src/modules/dids/DidsModuleConfig.ts b/packages/core/src/modules/dids/DidsModuleConfig.ts index 057772d8d8..8e065657b4 100644 --- a/packages/core/src/modules/dids/DidsModuleConfig.ts +++ b/packages/core/src/modules/dids/DidsModuleConfig.ts @@ -1,6 +1,14 @@ import type { DidRegistrar, DidResolver } from './domain' -import { KeyDidRegistrar, PeerDidRegistrar, KeyDidResolver, PeerDidResolver, WebDidResolver } from './methods' +import { + KeyDidRegistrar, + PeerDidRegistrar, + KeyDidResolver, + PeerDidResolver, + WebDidResolver, + JwkDidRegistrar, + JwkDidResolver, +} from './methods' /** * DidsModuleConfigOptions defines the interface for the options of the DidsModuleConfig class. @@ -15,7 +23,7 @@ export interface DidsModuleConfigOptions { * registered, as it is needed for the connections and out of band module to function. Other did methods can be * disabled. * - * @default [KeyDidRegistrar, PeerDidRegistrar] + * @default [KeyDidRegistrar, PeerDidRegistrar, JwkDidRegistrar] */ registrars?: DidRegistrar[] @@ -27,7 +35,7 @@ export interface DidsModuleConfigOptions { * registered, as it is needed for the connections and out of band module to function. Other did methods can be * disabled. * - * @default [WebDidResolver, KeyDidResolver, PeerDidResolver] + * @default [WebDidResolver, KeyDidResolver, PeerDidResolver, JwkDidResolver] */ resolvers?: DidResolver[] } @@ -46,7 +54,7 @@ export class DidsModuleConfig { // This prevents creating new instances every time this property is accessed if (this._registrars) return this._registrars - let registrars = this.options.registrars ?? [new KeyDidRegistrar(), new PeerDidRegistrar()] + let registrars = this.options.registrars ?? [new KeyDidRegistrar(), new PeerDidRegistrar(), new JwkDidRegistrar()] // Add peer did registrar if it is not included yet if (!registrars.find((registrar) => registrar instanceof PeerDidRegistrar)) { @@ -67,7 +75,12 @@ export class DidsModuleConfig { // This prevents creating new instances every time this property is accessed if (this._resolvers) return this._resolvers - let resolvers = this.options.resolvers ?? [new WebDidResolver(), new KeyDidResolver(), new PeerDidResolver()] + let resolvers = this.options.resolvers ?? [ + new WebDidResolver(), + new KeyDidResolver(), + new PeerDidResolver(), + new JwkDidResolver(), + ] // Add peer did resolver if it is not included yet if (!resolvers.find((resolver) => resolver instanceof PeerDidResolver)) { diff --git a/packages/core/src/modules/dids/__tests__/DidsModuleConfig.test.ts b/packages/core/src/modules/dids/__tests__/DidsModuleConfig.test.ts index 797a7f8615..ef3dc3dc66 100644 --- a/packages/core/src/modules/dids/__tests__/DidsModuleConfig.test.ts +++ b/packages/core/src/modules/dids/__tests__/DidsModuleConfig.test.ts @@ -1,17 +1,30 @@ import type { DidRegistrar, DidResolver } from '../domain' -import { KeyDidRegistrar, PeerDidRegistrar, KeyDidResolver, PeerDidResolver, WebDidResolver } from '..' import { DidsModuleConfig } from '../DidsModuleConfig' +import { + KeyDidRegistrar, + PeerDidRegistrar, + KeyDidResolver, + PeerDidResolver, + WebDidResolver, + JwkDidRegistrar, + JwkDidResolver, +} from '../methods' describe('DidsModuleConfig', () => { test('sets default values', () => { const config = new DidsModuleConfig() - expect(config.registrars).toEqual([expect.any(KeyDidRegistrar), expect.any(PeerDidRegistrar)]) + expect(config.registrars).toEqual([ + expect.any(KeyDidRegistrar), + expect.any(PeerDidRegistrar), + expect.any(JwkDidRegistrar), + ]) expect(config.resolvers).toEqual([ expect.any(WebDidResolver), expect.any(KeyDidResolver), expect.any(PeerDidResolver), + expect.any(JwkDidResolver), ]) }) diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP256.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP256.json new file mode 100644 index 0000000000..5465e191de --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP256.json @@ -0,0 +1,32 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/suites/jws-2020/v1"], + "id": "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv", + "verificationMethod": [ + { + "id": "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv", + "type": "JsonWebKey2020", + "controller": "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv", + "publicKeyJwk": { + "kty": "EC", + "crv": "P-256", + "x": "igrFmi0whuihKnj9R3Om1SoMph72wUGeFaBbzG2vzns", + "y": "efsX5b10x8yjyrj4ny3pGfLcY7Xby1KzgqOdqnsrJIM" + } + } + ], + "assertionMethod": [ + "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv" + ], + "authentication": [ + "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv" + ], + "capabilityInvocation": [ + "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv" + ], + "capabilityDelegation": [ + "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv" + ], + "keyAgreement": [ + "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv" + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP384.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP384.json new file mode 100644 index 0000000000..b5249b1afc --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP384.json @@ -0,0 +1,32 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/suites/jws-2020/v1"], + "id": "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9", + "verificationMethod": [ + { + "id": "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9", + "type": "JsonWebKey2020", + "controller": "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9", + "publicKeyJwk": { + "kty": "EC", + "crv": "P-384", + "x": "lInTxl8fjLKp_UCrxI0WDklahi-7-_6JbtiHjiRvMvhedhKVdHBfi2HCY8t_QJyc", + "y": "y6N1IC-2mXxHreETBW7K3mBcw0qGr3CWHCs-yl09yCQRLcyfGv7XhqAngHOu51Zv" + } + } + ], + "assertionMethod": [ + "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9" + ], + "authentication": [ + "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9" + ], + "capabilityInvocation": [ + "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9" + ], + "capabilityDelegation": [ + "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9" + ], + "keyAgreement": [ + "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9" + ] +} diff --git a/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP521.json b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP521.json new file mode 100644 index 0000000000..bafea05578 --- /dev/null +++ b/packages/core/src/modules/dids/__tests__/__fixtures__/didKeyP521.json @@ -0,0 +1,32 @@ +{ + "@context": ["https://w3id.org/did/v1", "https://w3id.org/security/suites/jws-2020/v1"], + "id": "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7", + "verificationMethod": [ + { + "id": "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7", + "type": "JsonWebKey2020", + "controller": "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7", + "publicKeyJwk": { + "kty": "EC", + "crv": "P-521", + "x": "ASUHPMyichQ0QbHZ9ofNx_l4y7luncn5feKLo3OpJ2nSbZoC7mffolj5uy7s6KSKXFmnNWxGJ42IOrjZ47qqwqyS", + "y": "AW9ziIC4ZQQVSNmLlp59yYKrjRY0_VqO-GOIYQ9tYpPraBKUloEId6cI_vynCzlZWZtWpgOM3HPhYEgawQ703RjC" + } + } + ], + "assertionMethod": [ + "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7" + ], + "authentication": [ + "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7" + ], + "capabilityInvocation": [ + "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7" + ], + "capabilityDelegation": [ + "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7" + ], + "keyAgreement": [ + "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7" + ] +} diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/ed25519.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/ed25519.test.ts index 0b654c1a53..c66b4fc7aa 100644 --- a/packages/core/src/modules/dids/domain/key-type/__tests__/ed25519.test.ts +++ b/packages/core/src/modules/dids/domain/key-type/__tests__/ed25519.test.ts @@ -55,7 +55,6 @@ describe('ed25519', () => { expect(keyDidEd25519.supportedVerificationMethodTypes).toMatchObject([ 'Ed25519VerificationKey2018', 'Ed25519VerificationKey2020', - 'JsonWebKey2020', ]) }) diff --git a/packages/core/src/modules/dids/domain/key-type/__tests__/jwk.test.ts b/packages/core/src/modules/dids/domain/key-type/__tests__/jwk.test.ts new file mode 100644 index 0000000000..aa186aef56 --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/__tests__/jwk.test.ts @@ -0,0 +1,42 @@ +import { Key } from '../../../../../crypto/Key' +import { JsonTransformer } from '../../../../../utils' +import didKeyP256Fixture from '../../../__tests__/__fixtures__/didKeyP256.json' +import { VerificationMethod } from '../../verificationMethod' +import { VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020 } from '../../verificationMethod/JsonWebKey2020' +import { keyDidJsonWebKey } from '../keyDidJsonWebKey' + +const TEST_P256_FINGERPRINT = 'zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv' +const TEST_P256_DID = `did:key:${TEST_P256_FINGERPRINT}` + +describe('keyDidJsonWebKey', () => { + it('should return a valid verification method', async () => { + const key = Key.fromFingerprint(TEST_P256_FINGERPRINT) + const verificationMethods = keyDidJsonWebKey.getVerificationMethods(TEST_P256_DID, key) + + expect(JsonTransformer.toJSON(verificationMethods)).toMatchObject([didKeyP256Fixture.verificationMethod[0]]) + }) + + it('supports no verification method type', () => { + expect(keyDidJsonWebKey.supportedVerificationMethodTypes).toMatchObject([ + VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, + ]) + }) + + it('returns key for JsonWebKey2020 verification method', () => { + const verificationMethod = JsonTransformer.fromJSON(didKeyP256Fixture.verificationMethod[0], VerificationMethod) + + const key = keyDidJsonWebKey.getKeyFromVerificationMethod(verificationMethod) + + expect(key.fingerprint).toBe(TEST_P256_FINGERPRINT) + }) + + it('throws an error if an invalid verification method is passed', () => { + const verificationMethod = JsonTransformer.fromJSON(didKeyP256Fixture.verificationMethod[0], VerificationMethod) + + verificationMethod.type = 'SomeRandomType' + + expect(() => keyDidJsonWebKey.getKeyFromVerificationMethod(verificationMethod)).toThrowError( + 'Invalid verification method passed' + ) + }) +}) diff --git a/packages/core/src/modules/dids/domain/key-type/ed25519.ts b/packages/core/src/modules/dids/domain/key-type/ed25519.ts index cee29095e6..4d96a43e6c 100644 --- a/packages/core/src/modules/dids/domain/key-type/ed25519.ts +++ b/packages/core/src/modules/dids/domain/key-type/ed25519.ts @@ -1,5 +1,4 @@ import type { KeyDidMapping } from './keyDidMapping' -import type { Jwk } from '../../../../crypto' import type { VerificationMethod } from '../verificationMethod' import { convertPublicKeyToX25519 } from '@stablelib/ed25519' @@ -8,7 +7,6 @@ import { Key, KeyType } from '../../../../crypto' export const VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018 = 'Ed25519VerificationKey2018' export const VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020 = 'Ed25519VerificationKey2020' -export const VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020 = 'JsonWebKey2020' export function getEd25519VerificationMethod({ key, id, controller }: { id: string; key: Key; controller: string }) { return { @@ -23,7 +21,6 @@ export const keyDidEd25519: KeyDidMapping = { supportedVerificationMethodTypes: [ VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2018, VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020, - VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, ], getVerificationMethods: (did, key) => [ getEd25519VerificationMethod({ id: `${did}#${key.fingerprint}`, key, controller: did }), @@ -35,15 +32,13 @@ export const keyDidEd25519: KeyDidMapping = { ) { return Key.fromPublicKeyBase58(verificationMethod.publicKeyBase58, KeyType.Ed25519) } + if ( verificationMethod.type === VERIFICATION_METHOD_TYPE_ED25519_VERIFICATION_KEY_2020 && verificationMethod.publicKeyMultibase ) { return Key.fromFingerprint(verificationMethod.publicKeyMultibase) } - if (verificationMethod.type === VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020 && verificationMethod.publicKeyJwk) { - return Key.fromJwk(verificationMethod.publicKeyJwk as unknown as Jwk) - } throw new Error('Invalid verification method passed') }, diff --git a/packages/core/src/modules/dids/domain/key-type/keyDidJsonWebKey.ts b/packages/core/src/modules/dids/domain/key-type/keyDidJsonWebKey.ts new file mode 100644 index 0000000000..3dd7c19d05 --- /dev/null +++ b/packages/core/src/modules/dids/domain/key-type/keyDidJsonWebKey.ts @@ -0,0 +1,20 @@ +import type { KeyDidMapping } from './keyDidMapping' +import type { VerificationMethod } from '../verificationMethod' + +import { Key } from '../../../../crypto' +import { AriesFrameworkError } from '../../../../error' +import { getJsonWebKey2020VerificationMethod } from '../verificationMethod' +import { VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, isJsonWebKey2020 } from '../verificationMethod/JsonWebKey2020' + +export const keyDidJsonWebKey: KeyDidMapping = { + supportedVerificationMethodTypes: [VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020], + getVerificationMethods: (did, key) => [getJsonWebKey2020VerificationMethod({ did, key })], + + getKeyFromVerificationMethod: (verificationMethod: VerificationMethod) => { + if (!isJsonWebKey2020(verificationMethod) || !verificationMethod.publicKeyJwk) { + throw new AriesFrameworkError('Invalid verification method passed') + } + + return Key.fromJwk(verificationMethod.publicKeyJwk) + }, +} diff --git a/packages/core/src/modules/dids/domain/key-type/keyDidMapping.ts b/packages/core/src/modules/dids/domain/key-type/keyDidMapping.ts index bb788c8532..1449892e9b 100644 --- a/packages/core/src/modules/dids/domain/key-type/keyDidMapping.ts +++ b/packages/core/src/modules/dids/domain/key-type/keyDidMapping.ts @@ -1,12 +1,15 @@ -import type { Key } from '../../../../crypto/Key' import type { VerificationMethod } from '../verificationMethod' import { KeyType } from '../../../../crypto' +import { Key } from '../../../../crypto/Key' +import { AriesFrameworkError } from '../../../../error' +import { isJsonWebKey2020, VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020 } from '../verificationMethod/JsonWebKey2020' import { keyDidBls12381g1 } from './bls12381g1' import { keyDidBls12381g1g2 } from './bls12381g1g2' import { keyDidBls12381g2 } from './bls12381g2' import { keyDidEd25519 } from './ed25519' +import { keyDidJsonWebKey } from './keyDidJsonWebKey' import { keyDidX25519 } from './x25519' export interface KeyDidMapping { @@ -22,6 +25,9 @@ const keyDidMapping: Record = { [KeyType.Bls12381g1]: keyDidBls12381g1, [KeyType.Bls12381g2]: keyDidBls12381g2, [KeyType.Bls12381g1g2]: keyDidBls12381g1g2, + [KeyType.P256]: keyDidJsonWebKey, + [KeyType.P384]: keyDidJsonWebKey, + [KeyType.P521]: keyDidJsonWebKey, } /** @@ -61,8 +67,19 @@ export function getKeyDidMappingByKeyType(keyType: KeyType) { } export function getKeyFromVerificationMethod(verificationMethod: VerificationMethod) { - const keyDid = verificationMethodKeyDidMapping[verificationMethod.type] + // This is a special verification method, as it supports basically all key types. + if (isJsonWebKey2020(verificationMethod)) { + // TODO: move this validation to another place + if (!verificationMethod.publicKeyJwk) { + throw new AriesFrameworkError( + `Missing publicKeyJwk on verification method with type ${VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020}` + ) + } + return Key.fromJwk(verificationMethod.publicKeyJwk) + } + + const keyDid = verificationMethodKeyDidMapping[verificationMethod.type] if (!keyDid) { throw new Error(`Unsupported key did from verification method type '${verificationMethod.type}'`) } diff --git a/packages/core/src/modules/dids/domain/keyDidDocument.ts b/packages/core/src/modules/dids/domain/keyDidDocument.ts index 537cb97d3d..af97546202 100644 --- a/packages/core/src/modules/dids/domain/keyDidDocument.ts +++ b/packages/core/src/modules/dids/domain/keyDidDocument.ts @@ -1,7 +1,9 @@ +import type { DidDocument } from './DidDocument' import type { VerificationMethod } from './verificationMethod/VerificationMethod' import { KeyType, Key } from '../../../crypto' -import { SECURITY_CONTEXT_BBS_URL, SECURITY_X25519_CONTEXT_URL } from '../../vc/constants' +import { AriesFrameworkError } from '../../../error' +import { SECURITY_CONTEXT_BBS_URL, SECURITY_JWS_CONTEXT_URL, SECURITY_X25519_CONTEXT_URL } from '../../vc/constants' import { ED25519_SUITE_CONTEXT_URL_2018 } from '../../vc/signature-suites/ed25519/constants' import { DidDocumentBuilder } from './DidDocumentBuilder' @@ -10,13 +12,17 @@ import { getBls12381g1g2VerificationMethod } from './key-type/bls12381g1g2' import { getBls12381g2VerificationMethod } from './key-type/bls12381g2' import { convertPublicKeyToX25519, getEd25519VerificationMethod } from './key-type/ed25519' import { getX25519VerificationMethod } from './key-type/x25519' +import { getJsonWebKey2020VerificationMethod } from './verificationMethod' -const didDocumentKeyTypeMapping = { +const didDocumentKeyTypeMapping: Record DidDocument> = { [KeyType.Ed25519]: getEd25519DidDoc, [KeyType.X25519]: getX25519DidDoc, [KeyType.Bls12381g1]: getBls12381g1DidDoc, [KeyType.Bls12381g2]: getBls12381g2DidDoc, [KeyType.Bls12381g1g2]: getBls12381g1g2DidDoc, + [KeyType.P256]: getJsonWebKey2020DidDocument, + [KeyType.P384]: getJsonWebKey2020DidDocument, + [KeyType.P521]: getJsonWebKey2020DidDocument, } export function getDidDocumentForKey(did: string, key: Key) { @@ -54,6 +60,31 @@ function getBls12381g1g2DidDoc(did: string, key: Key) { return didDocumentBuilder.addContext(SECURITY_CONTEXT_BBS_URL).build() } +export function getJsonWebKey2020DidDocument(did: string, key: Key) { + const verificationMethod = getJsonWebKey2020VerificationMethod({ did, key }) + + const didDocumentBuilder = new DidDocumentBuilder(did) + didDocumentBuilder.addContext(SECURITY_JWS_CONTEXT_URL).addVerificationMethod(verificationMethod) + + if (!key.supportsEncrypting && !key.supportsSigning) { + throw new AriesFrameworkError('Key must support at least signing or encrypting') + } + + if (key.supportsSigning) { + didDocumentBuilder + .addAuthentication(verificationMethod.id) + .addAssertionMethod(verificationMethod.id) + .addCapabilityDelegation(verificationMethod.id) + .addCapabilityInvocation(verificationMethod.id) + } + + if (key.supportsEncrypting) { + didDocumentBuilder.addKeyAgreement(verificationMethod.id) + } + + return didDocumentBuilder.build() +} + function getEd25519DidDoc(did: string, key: Key) { const verificationMethod = getEd25519VerificationMethod({ id: `${did}#${key.fingerprint}`, key, controller: did }) diff --git a/packages/core/src/modules/dids/domain/verificationMethod/JsonWebKey2020.ts b/packages/core/src/modules/dids/domain/verificationMethod/JsonWebKey2020.ts new file mode 100644 index 0000000000..a007f1d1fc --- /dev/null +++ b/packages/core/src/modules/dids/domain/verificationMethod/JsonWebKey2020.ts @@ -0,0 +1,36 @@ +import type { VerificationMethod } from './VerificationMethod' +import type { Jwk } from '../../../../crypto' + +import { Key } from '../../../../crypto' + +export const VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020 = 'JsonWebKey2020' + +type JwkOrKey = { jwk: Jwk; key?: never } | { key: Key; jwk?: never } +type GetJsonWebKey2020VerificationMethodOptions = { + did: string + + verificationMethodId?: string +} & JwkOrKey + +export function getJsonWebKey2020VerificationMethod({ + did, + key, + jwk, + verificationMethodId, +}: GetJsonWebKey2020VerificationMethodOptions) { + if (!verificationMethodId) { + const k = key ?? Key.fromJwk(jwk) + verificationMethodId = `${did}#${k.fingerprint}` + } + + return { + id: verificationMethodId, + type: VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020, + controller: did, + publicKeyJwk: jwk ?? key.toJwk(), + } +} + +export function isJsonWebKey2020(verificationMethod: VerificationMethod) { + return verificationMethod.type === VERIFICATION_METHOD_TYPE_JSON_WEB_KEY_2020 +} diff --git a/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethod.ts b/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethod.ts index a86bd58978..632596fd57 100644 --- a/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethod.ts +++ b/packages/core/src/modules/dids/domain/verificationMethod/VerificationMethod.ts @@ -1,3 +1,5 @@ +import type { Jwk } from '../../../../crypto' + import { IsString, IsOptional } from 'class-validator' export interface VerificationMethodOptions { @@ -6,7 +8,7 @@ export interface VerificationMethodOptions { controller: string publicKeyBase58?: string publicKeyBase64?: string - publicKeyJwk?: Record + publicKeyJwk?: Jwk publicKeyHex?: string publicKeyMultibase?: string publicKeyPem?: string @@ -48,8 +50,8 @@ export class VerificationMethod { @IsString() public publicKeyBase64?: string - // TODO: define JWK structure, we don't support JWK yet - public publicKeyJwk?: Record + // TODO: validation of JWK + public publicKeyJwk?: Jwk @IsOptional() @IsString() diff --git a/packages/core/src/modules/dids/domain/verificationMethod/index.ts b/packages/core/src/modules/dids/domain/verificationMethod/index.ts index 2bfdad4059..b263061277 100644 --- a/packages/core/src/modules/dids/domain/verificationMethod/index.ts +++ b/packages/core/src/modules/dids/domain/verificationMethod/index.ts @@ -1,4 +1,10 @@ +import { getJsonWebKey2020VerificationMethod } from './JsonWebKey2020' import { VerificationMethod } from './VerificationMethod' import { VerificationMethodTransformer, IsStringOrVerificationMethod } from './VerificationMethodTransformer' -export { VerificationMethod, VerificationMethodTransformer, IsStringOrVerificationMethod } +export { + VerificationMethod, + VerificationMethodTransformer, + IsStringOrVerificationMethod, + getJsonWebKey2020VerificationMethod, +} diff --git a/packages/core/src/modules/dids/methods/index.ts b/packages/core/src/modules/dids/methods/index.ts index 12f78247af..4faee9c44b 100644 --- a/packages/core/src/modules/dids/methods/index.ts +++ b/packages/core/src/modules/dids/methods/index.ts @@ -1,3 +1,4 @@ export * from './key' export * from './peer' export * from './web' +export * from './jwk' diff --git a/packages/core/src/modules/dids/methods/jwk/DidJwk.ts b/packages/core/src/modules/dids/methods/jwk/DidJwk.ts new file mode 100644 index 0000000000..f83b956e24 --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/DidJwk.ts @@ -0,0 +1,55 @@ +import type { Jwk } from '../../../../crypto' + +import { Key } from '../../../../crypto/Key' +import { JsonEncoder } from '../../../../utils' +import { parseDid } from '../../domain/parse' + +import { getDidJwkDocument } from './didJwkDidDocument' + +export class DidJwk { + public readonly did: string + + private constructor(did: string) { + this.did = did + } + + public get allowsEncrypting() { + return this.jwk.use === 'enc' || this.key.supportsEncrypting + } + + public get allowsSigning() { + return this.jwk.use === 'sig' || this.key.supportsSigning + } + + public static fromDid(did: string) { + // We create a `Key` instance form the jwk, as that validates the jwk + const parsed = parseDid(did) + const jwk = JsonEncoder.fromBase64(parsed.id) as Jwk + Key.fromJwk(jwk) + + return new DidJwk(did) + } + + public static fromJwk(jwk: Jwk) { + // We create a `Key` instance form the jwk, as that validates the jwk + Key.fromJwk(jwk) + const did = `did:jwk:${JsonEncoder.toBase64URL(jwk)}` + + return new DidJwk(did) + } + + public get key() { + return Key.fromJwk(this.jwk) + } + + public get jwk() { + const parsed = parseDid(this.did) + const jwk = JsonEncoder.fromBase64(parsed.id) as Jwk + + return jwk + } + + public get didDocument() { + return getDidJwkDocument(this) + } +} diff --git a/packages/core/src/modules/dids/methods/jwk/JwkDidRegistrar.ts b/packages/core/src/modules/dids/methods/jwk/JwkDidRegistrar.ts new file mode 100644 index 0000000000..c5fc9b01e0 --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/JwkDidRegistrar.ts @@ -0,0 +1,118 @@ +import type { AgentContext } from '../../../../agent' +import type { KeyType } from '../../../../crypto' +import type { Buffer } from '../../../../utils' +import type { DidRegistrar } from '../../domain/DidRegistrar' +import type { DidCreateOptions, DidCreateResult, DidDeactivateResult, DidUpdateResult } from '../../types' + +import { DidDocumentRole } from '../../domain/DidDocumentRole' +import { DidRepository, DidRecord } from '../../repository' + +import { DidJwk } from './DidJwk' + +export class JwkDidRegistrar implements DidRegistrar { + public readonly supportedMethods = ['jwk'] + + public async create(agentContext: AgentContext, options: JwkDidCreateOptions): Promise { + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + + const keyType = options.options.keyType + const seed = options.secret?.seed + const privateKey = options.secret?.privateKey + + if (!keyType) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Missing key type', + }, + } + } + + try { + const key = await agentContext.wallet.createKey({ + keyType, + seed, + privateKey, + }) + + const didJwk = DidJwk.fromJwk(key.toJwk()) + + // Save the did so we know we created it and can issue with it + const didRecord = new DidRecord({ + did: didJwk.did, + role: DidDocumentRole.Created, + }) + await didRepository.save(agentContext, didRecord) + + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: didJwk.did, + didDocument: didJwk.didDocument, + secret: { + // FIXME: the uni-registrar creates the seed in the registrar method + // if it doesn't exist so the seed can always be returned. Currently + // we can only return it if the seed was passed in by the user. Once + // we have a secure method for generating seeds we should use the same + // approach + seed: options.secret?.seed, + privateKey: options.secret?.privateKey, + }, + }, + } + } catch (error) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async update(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notSupported: cannot update did:jwk did`, + }, + } + } + + public async deactivate(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notSupported: cannot deactivate did:jwk did`, + }, + } + } +} + +export interface JwkDidCreateOptions extends DidCreateOptions { + method: 'jwk' + // For now we don't support creating a did:jwk with a did or did document + did?: never + didDocument?: never + options: { + keyType: KeyType + } + secret?: { + seed?: Buffer + privateKey?: Buffer + } +} + +// Update and Deactivate not supported for did:jwk +export type JwkDidUpdateOptions = never +export type JwkDidDeactivateOptions = never diff --git a/packages/core/src/modules/dids/methods/jwk/JwkDidResolver.ts b/packages/core/src/modules/dids/methods/jwk/JwkDidResolver.ts new file mode 100644 index 0000000000..8207814895 --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/JwkDidResolver.ts @@ -0,0 +1,32 @@ +import type { AgentContext } from '../../../../agent' +import type { DidResolver } from '../../domain/DidResolver' +import type { DidResolutionResult } from '../../types' + +import { DidJwk } from './DidJwk' + +export class JwkDidResolver implements DidResolver { + public readonly supportedMethods = ['jwk'] + + public async resolve(agentContext: AgentContext, did: string): Promise { + const didDocumentMetadata = {} + + try { + const didDocument = DidJwk.fromDid(did).didDocument + + return { + didDocument, + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } catch (error) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}': ${error}`, + }, + } + } + } +} diff --git a/packages/core/src/modules/dids/methods/jwk/__tests__/DidJwk.test.ts b/packages/core/src/modules/dids/methods/jwk/__tests__/DidJwk.test.ts new file mode 100644 index 0000000000..406abe86a8 --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/__tests__/DidJwk.test.ts @@ -0,0 +1,23 @@ +import { DidJwk } from '../DidJwk' + +import { p256DidJwkEyJjcnYi0iFixture } from './__fixtures__/p256DidJwkEyJjcnYi0i' +import { x25519DidJwkEyJrdHkiOiJFixture } from './__fixtures__/x25519DidJwkEyJrdHkiOiJ' + +describe('DidJwk', () => { + it('creates a DidJwk instance from a did', async () => { + const documentTypes = [p256DidJwkEyJjcnYi0iFixture, x25519DidJwkEyJrdHkiOiJFixture] + + for (const documentType of documentTypes) { + const didKey = DidJwk.fromDid(documentType.id) + + expect(didKey.didDocument.toJSON()).toMatchObject(documentType) + } + }) + + it('creates a DidJwk instance from a jwk instance', async () => { + const didJwk = DidJwk.fromJwk(p256DidJwkEyJjcnYi0iFixture.verificationMethod[0].publicKeyJwk) + + expect(didJwk.did).toBe(p256DidJwkEyJjcnYi0iFixture.id) + expect(didJwk.didDocument.toJSON()).toMatchObject(p256DidJwkEyJjcnYi0iFixture) + }) +}) diff --git a/packages/core/src/modules/dids/methods/jwk/__tests__/JwkDidRegistrar.test.ts b/packages/core/src/modules/dids/methods/jwk/__tests__/JwkDidRegistrar.test.ts new file mode 100644 index 0000000000..dc4f246b99 --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/__tests__/JwkDidRegistrar.test.ts @@ -0,0 +1,193 @@ +import type { Wallet } from '../../../../../wallet' + +import { getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { KeyType } from '../../../../../crypto' +import { Key } from '../../../../../crypto/Key' +import { TypedArrayEncoder } from '../../../../../utils' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { WalletError } from '../../../../../wallet/error' +import { DidDocumentRole } from '../../../domain/DidDocumentRole' +import { DidRepository } from '../../../repository/DidRepository' +import { JwkDidRegistrar } from '../JwkDidRegistrar' + +jest.mock('../../../repository/DidRepository') +const DidRepositoryMock = DidRepository as jest.Mock + +const walletMock = { + createKey: jest.fn(() => + Key.fromJwk({ + crv: 'P-256', + kty: 'EC', + x: 'acbIQiuMs3i8_uszEjJ2tpTtRM4EU3yz91PH6CdH2V0', + y: '_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE', + }) + ), +} as unknown as Wallet + +const didRepositoryMock = new DidRepositoryMock() +const jwkDidRegistrar = new JwkDidRegistrar() + +const agentContext = getAgentContext({ + wallet: walletMock, + registerInstances: [[DidRepository, didRepositoryMock]], +}) + +describe('DidRegistrar', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('JwkDidRegistrar', () => { + it('should correctly create a did:jwk document using P256 key type', async () => { + const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + + const result = await jwkDidRegistrar.create(agentContext, { + method: 'jwk', + options: { + keyType: KeyType.P256, + }, + secret: { + privateKey, + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9', + didDocument: { + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9', + verificationMethod: [ + { + id: 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + type: 'JsonWebKey2020', + controller: + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9', + publicKeyJwk: { + crv: 'P-256', + kty: 'EC', + x: 'acbIQiuMs3i8_uszEjJ2tpTtRM4EU3yz91PH6CdH2V0', + y: '_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE', + }, + }, + ], + assertionMethod: [ + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + authentication: [ + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + capabilityInvocation: [ + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + capabilityDelegation: [ + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + keyAgreement: [ + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + }, + secret: { + privateKey, + }, + }, + }) + + expect(walletMock.createKey).toHaveBeenCalledWith({ keyType: KeyType.P256, privateKey }) + }) + + it('should return an error state if no key type is provided', async () => { + const result = await jwkDidRegistrar.create(agentContext, { + method: 'jwk', + // @ts-expect-error - key type is required in interface + options: {}, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Missing key type', + }, + }) + }) + + it('should return an error state if a key creation error is thrown', async () => { + mockFunction(walletMock.createKey).mockRejectedValueOnce(new WalletError('Invalid private key provided')) + const result = await jwkDidRegistrar.create(agentContext, { + method: 'jwk', + options: { + keyType: KeyType.P256, + }, + secret: { + privateKey: TypedArrayEncoder.fromString('invalid'), + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: expect.stringContaining('Invalid private key provided'), + }, + }) + }) + + it('should store the did document', async () => { + const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + const did = + 'did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9' + + await jwkDidRegistrar.create(agentContext, { + method: 'jwk', + + options: { + keyType: KeyType.P256, + }, + secret: { + privateKey, + }, + }) + + expect(didRepositoryMock.save).toHaveBeenCalledTimes(1) + const [, didRecord] = mockFunction(didRepositoryMock.save).mock.calls[0] + + expect(didRecord).toMatchObject({ + did, + role: DidDocumentRole.Created, + didDocument: undefined, + }) + }) + + it('should return an error state when calling update', async () => { + const result = await jwkDidRegistrar.update() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notSupported: cannot update did:jwk did`, + }, + }) + }) + + it('should return an error state when calling deactivate', async () => { + const result = await jwkDidRegistrar.deactivate() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notSupported: cannot deactivate did:jwk did`, + }, + }) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/jwk/__tests__/JwkDidResolver.test.ts b/packages/core/src/modules/dids/methods/jwk/__tests__/JwkDidResolver.test.ts new file mode 100644 index 0000000000..28dc34d497 --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/__tests__/JwkDidResolver.test.ts @@ -0,0 +1,34 @@ +import type { AgentContext } from '../../../../../agent' + +import { getAgentContext } from '../../../../../../tests/helpers' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { DidJwk } from '../DidJwk' +import { JwkDidResolver } from '../JwkDidResolver' + +import { p256DidJwkEyJjcnYi0iFixture } from './__fixtures__/p256DidJwkEyJjcnYi0i' + +describe('DidResolver', () => { + describe('JwkDidResolver', () => { + let keyDidResolver: JwkDidResolver + let agentContext: AgentContext + + beforeEach(() => { + keyDidResolver = new JwkDidResolver() + agentContext = getAgentContext() + }) + + it('should correctly resolve a did:jwk document', async () => { + const fromDidSpy = jest.spyOn(DidJwk, 'fromDid') + const result = await keyDidResolver.resolve(agentContext, p256DidJwkEyJjcnYi0iFixture.id) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: p256DidJwkEyJjcnYi0iFixture, + didDocumentMetadata: {}, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + }) + expect(result.didDocument) + expect(fromDidSpy).toHaveBeenCalledTimes(1) + expect(fromDidSpy).toHaveBeenCalledWith(p256DidJwkEyJjcnYi0iFixture.id) + }) + }) +}) diff --git a/packages/core/src/modules/dids/methods/jwk/__tests__/__fixtures__/p256DidJwkEyJjcnYi0i.ts b/packages/core/src/modules/dids/methods/jwk/__tests__/__fixtures__/p256DidJwkEyJjcnYi0i.ts new file mode 100644 index 0000000000..d086154f38 --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/__tests__/__fixtures__/p256DidJwkEyJjcnYi0i.ts @@ -0,0 +1,33 @@ +export const p256DidJwkEyJjcnYi0iFixture = { + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9', + verificationMethod: [ + { + id: 'did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + type: 'JsonWebKey2020', + controller: + 'did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9', + publicKeyJwk: { + crv: 'P-256', + kty: 'EC', + x: 'acbIQiuMs3i8_uszEjJ2tpTtRM4EU3yz91PH6CdH2V0', + y: '_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE', + }, + }, + ], + assertionMethod: [ + 'did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + authentication: [ + 'did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + capabilityInvocation: [ + 'did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + capabilityDelegation: [ + 'did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], + keyAgreement: [ + 'did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0', + ], +} as const diff --git a/packages/core/src/modules/dids/methods/jwk/__tests__/__fixtures__/x25519DidJwkEyJrdHkiOiJ.ts b/packages/core/src/modules/dids/methods/jwk/__tests__/__fixtures__/x25519DidJwkEyJrdHkiOiJ.ts new file mode 100644 index 0000000000..dba397342f --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/__tests__/__fixtures__/x25519DidJwkEyJrdHkiOiJ.ts @@ -0,0 +1,21 @@ +export const x25519DidJwkEyJrdHkiOiJFixture = { + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9', + verificationMethod: [ + { + id: 'did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0', + type: 'JsonWebKey2020', + controller: + 'did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9', + publicKeyJwk: { + kty: 'OKP', + crv: 'X25519', + use: 'enc', + x: '3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08', + }, + }, + ], + keyAgreement: [ + 'did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0', + ], +} as const diff --git a/packages/core/src/modules/dids/methods/jwk/didJwkDidDocument.ts b/packages/core/src/modules/dids/methods/jwk/didJwkDidDocument.ts new file mode 100644 index 0000000000..3906929375 --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/didJwkDidDocument.ts @@ -0,0 +1,35 @@ +import type { DidJwk } from './DidJwk' + +import { AriesFrameworkError } from '../../../../error' +import { SECURITY_JWS_CONTEXT_URL } from '../../../vc/constants' +import { getJsonWebKey2020VerificationMethod, DidDocumentBuilder } from '../../domain' + +export function getDidJwkDocument(didJwk: DidJwk) { + if (!didJwk.allowsEncrypting && !didJwk.allowsSigning) { + throw new AriesFrameworkError('At least one of allowsSigning or allowsEncrypting must be enabled') + } + + const verificationMethod = getJsonWebKey2020VerificationMethod({ + did: didJwk.did, + jwk: didJwk.jwk, + verificationMethodId: `${didJwk.did}#0`, + }) + + const didDocumentBuilder = new DidDocumentBuilder(didJwk.did) + .addContext(SECURITY_JWS_CONTEXT_URL) + .addVerificationMethod(verificationMethod) + + if (didJwk.allowsSigning) { + didDocumentBuilder + .addAuthentication(verificationMethod.id) + .addAssertionMethod(verificationMethod.id) + .addCapabilityDelegation(verificationMethod.id) + .addCapabilityInvocation(verificationMethod.id) + } + + if (didJwk.allowsEncrypting) { + didDocumentBuilder.addKeyAgreement(verificationMethod.id) + } + + return didDocumentBuilder.build() +} diff --git a/packages/core/src/modules/dids/methods/jwk/index.ts b/packages/core/src/modules/dids/methods/jwk/index.ts new file mode 100644 index 0000000000..e377f85f95 --- /dev/null +++ b/packages/core/src/modules/dids/methods/jwk/index.ts @@ -0,0 +1,3 @@ +export { DidJwk } from './DidJwk' +export * from './JwkDidRegistrar' +export * from './JwkDidResolver' diff --git a/packages/core/src/modules/dids/methods/key/__tests__/DidKey.test.ts b/packages/core/src/modules/dids/methods/key/__tests__/DidKey.test.ts index bacfb3f1a9..a9a854cb1a 100644 --- a/packages/core/src/modules/dids/methods/key/__tests__/DidKey.test.ts +++ b/packages/core/src/modules/dids/methods/key/__tests__/DidKey.test.ts @@ -4,12 +4,24 @@ import didKeyBls12381g1 from '../../../__tests__/__fixtures__/didKeyBls12381g1.j import didKeyBls12381g1g2 from '../../../__tests__/__fixtures__/didKeyBls12381g1g2.json' import didKeyBls12381g2 from '../../../__tests__/__fixtures__/didKeyBls12381g2.json' import didKeyEd25519 from '../../../__tests__/__fixtures__/didKeyEd25519.json' +import didKeyP256 from '../../../__tests__/__fixtures__/didKeyP256.json' +import didKeyP384 from '../../../__tests__/__fixtures__/didKeyP384.json' +import didKeyP521 from '../../../__tests__/__fixtures__/didKeyP521.json' import didKeyX25519 from '../../../__tests__/__fixtures__/didKeyX25519.json' import { DidKey } from '../DidKey' describe('DidKey', () => { it('creates a DidKey instance from a did', async () => { - const documentTypes = [didKeyX25519, didKeyEd25519, didKeyBls12381g1, didKeyBls12381g2, didKeyBls12381g1g2] + const documentTypes = [ + didKeyX25519, + didKeyEd25519, + didKeyBls12381g1, + didKeyBls12381g2, + didKeyBls12381g1g2, + didKeyP256, + didKeyP384, + didKeyP521, + ] for (const documentType of documentTypes) { const didKey = DidKey.fromDid(documentType.id) diff --git a/packages/core/src/modules/vc/constants.ts b/packages/core/src/modules/vc/constants.ts index b166244ebf..a9636cf016 100644 --- a/packages/core/src/modules/vc/constants.ts +++ b/packages/core/src/modules/vc/constants.ts @@ -13,3 +13,4 @@ export const SECURITY_SIGNATURE_URL = 'https://w3id.org/security#signature' export const VERIFIABLE_CREDENTIAL_TYPE = 'VerifiableCredential' export const VERIFIABLE_PRESENTATION_TYPE = 'VerifiablePresentation' export const EXPANDED_TYPE_CREDENTIALS_CONTEXT_V1_VC_TYPE = 'https://www.w3.org/2018/credentials#VerifiableCredential' +export const SECURITY_JWS_CONTEXT_URL = 'https://w3id.org/security/suites/jws-2020/v1' diff --git a/packages/openid4vc-client/src/OpenId4VcClientService.ts b/packages/openid4vc-client/src/OpenId4VcClientService.ts index b458cd631d..629e1d589f 100644 --- a/packages/openid4vc-client/src/OpenId4VcClientService.ts +++ b/packages/openid4vc-client/src/OpenId4VcClientService.ts @@ -257,12 +257,11 @@ export class OpenId4VcClientService { this.logger.debug('Full server metadata', serverMetadata) - if (!accessToken.scope) { - throw new AriesFrameworkError( - "Access token response doesn't contain a scope. Only scoped issuer URIs are supported at this time." - ) + if (accessToken.scope) { + for (const credentialType of accessToken.scope.split(' ')) { + this.assertCredentialHasFormat(credentialFormat, credentialType, serverMetadata) + } } - this.assertCredentialHasFormat(credentialFormat, accessToken.scope, serverMetadata) // proof of possession const callbacks = this.getSignCallback(agentContext) diff --git a/yarn.lock b/yarn.lock index 5b6b66c9c0..87c7ee9ec5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3604,6 +3604,11 @@ before-after-hook@^2.2.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== +big-integer@^1.6.51: + version "1.6.51" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" + integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== + bignumber.js@^9.0.0: version "9.1.1" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6"