Skip to content

Commit

Permalink
bumping web5 versions. removed jwt related methods and credential met…
Browse files Browse the repository at this point in the history
…hods in dev-tools
  • Loading branch information
jiyoontbd committed Jan 20, 2024
1 parent faf4510 commit 7f56d82
Show file tree
Hide file tree
Showing 8 changed files with 372 additions and 246 deletions.
16 changes: 10 additions & 6 deletions packages/http-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,21 @@
},
"dependencies": {
"@tbdex/protocol": "workspace:*",
"@web5/common": "0.2.1",
"@web5/crypto": "0.2.2",
"@web5/dids": "0.2.2",
"query-string": "8.1.0"
"@web5/common": "0.2.2",
"@web5/credentials": "0.4.1",
"@web5/crypto": "0.2.4",
"@web5/dids": "0.2.4",
"ms": "2.1.3",
"query-string": "8.1.0",
"typeid-js": "0.3.0"
},
"devDependencies": {
"@playwright/test": "1.34.3",
"@types/chai": "4.3.5",
"@types/eslint": "8.37.0",
"@types/mocha": "10.0.1",
"@types/sinon": "^17.0.2",
"@types/ms": "0.7.34",
"@types/sinon": "17.0.2",
"@typescript-eslint/eslint-plugin": "5.59.0",
"@typescript-eslint/parser": "5.59.0",
"chai": "4.3.10",
Expand All @@ -78,7 +82,7 @@
"mkdirp": "3.0.1",
"mocha": "10.2.0",
"rimraf": "4.4.0",
"sinon": "15.0.2",
"sinon": "17.0.1",
"typedoc": "0.25.0",
"typedoc-plugin-markdown": "3.16.0",
"typescript": "5.2.2"
Expand Down
11 changes: 6 additions & 5 deletions packages/protocol/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,18 @@
},
"dependencies": {
"@noble/hashes": "1.3.2",
"@web5/common": "0.2.1",
"@web5/credentials": "0.3.2",
"@web5/crypto": "0.2.2",
"@web5/dids": "0.2.2",
"@web5/common": "0.2.2",
"@web5/credentials": "0.4.1",
"@web5/crypto": "0.2.4",
"@web5/dids": "0.2.4",
"ajv": "8.12.0",
"bignumber.js": "^9.1.2",
"canonicalize": "2.0.0",
"typeid-js": "0.3.0"
},
"devDependencies": {
"@playwright/test": "1.34.3",
"@types/sinon": "17.0.2",
"chai": "4.3.10",
"esbuild": "0.16.17",
"karma": "6.4.1",
Expand All @@ -75,7 +76,7 @@
"mocha": "10.2.0",
"node-stdlib-browser": "1.2.0",
"rimraf": "4.4.0",
"sinon": "15.0.2",
"sinon": "17.0.1",
"typedoc": "0.25.0",
"typedoc-plugin-markdown": "3.16.0",
"typescript": "5.2.2"
Expand Down
58 changes: 20 additions & 38 deletions packages/protocol/src/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import type {
PrivateKeyJwk as Web5PrivateKeyJwk,
CryptoAlgorithm,
Web5Crypto,
JwsHeaderParams,
PrivateKeyJwk,
PublicKeyJwk
JwkParamsEcPrivate,
JwkParamsOkpPrivate,
JwkParamsEcPublic,
JwkParamsOkpPublic
} from '@web5/crypto'

import { sha256 } from '@noble/hashes/sha256'
import { Convert } from '@web5/common'
import { EcdsaAlgorithm, EdDsaAlgorithm, Jose } from '@web5/crypto'
import { EcdsaAlgorithm, EdDsaAlgorithm } from '@web5/crypto'
import { deferenceDidUrl, isVerificationMethod } from './did-resolver.js'

import canonicalize from 'canonicalize'
Expand Down Expand Up @@ -44,14 +45,14 @@ export type VerifyOptions = {
*/
type SignerValue<T extends Web5Crypto.Algorithm> = {
signer: CryptoAlgorithm,
options: T,
options?: T,
alg: JwsHeader['alg'],
crv: JsonWebKey['crv']
}

const secp256k1Signer: SignerValue<Web5Crypto.EcdsaOptions> = {
signer : new EcdsaAlgorithm(),
options : { name: 'ECDSA', hash: 'SHA-256' },
options : { name: 'ECDSA' },
alg : 'ES256K',
crv : 'secp256k1'
}
Expand Down Expand Up @@ -101,32 +102,30 @@ export class Crypto {
static async sign(opts: SignOptions) {
const { did, payload, detached } = opts

const privateKeyJwk = did.keySet.verificationMethodKeys?.[0]?.privateKeyJwk
const privateKeyJwk = did.keySet.verificationMethodKeys[0].privateKeyJwk as JwkParamsEcPrivate | JwkParamsOkpPrivate

const algorithmName = privateKeyJwk?.['alg'] || ''
let namedCurve = Crypto.extractNamedCurve(privateKeyJwk)
const algorithmName = privateKeyJwk['alg'] || ''
const namedCurve = privateKeyJwk['crv'] || ''
const algorithmId = `${algorithmName}:${namedCurve}`

const algorithm = this.algorithms[algorithmId]
if (!algorithm) {
throw new Error(`Algorithm (${algorithmId}) not supported`)
throw new Error(`${algorithmId} not supported`)
}

let verificationMethodId = did.document.verificationMethod?.[0]?.id || ''
let verificationMethodId = did.document.verificationMethod[0].id
if (verificationMethodId.startsWith('#')) {
verificationMethodId = `${did.did}#${verificationMethodId}`
verificationMethodId = `${did.did}${verificationMethodId}`
}

const jwsHeader: JwsHeader = { alg: algorithm.alg, kid: verificationMethodId }
const base64UrlEncodedJwsHeader = Convert.object(jwsHeader).toBase64Url()
const base64urlEncodedJwsPayload = Convert.uint8Array(payload).toBase64Url()

const key = await Jose.jwkToCryptoKey({ key: privateKeyJwk as Web5PrivateKeyJwk })

const toSign = `${base64UrlEncodedJwsHeader}.${base64urlEncodedJwsPayload}`
const toSignBytes = Convert.string(toSign).toUint8Array()

const signatureBytes = await algorithm.signer.sign({ key, data: toSignBytes, algorithm: algorithm.options })
const signatureBytes = await algorithm.signer.sign({ key: privateKeyJwk, data: toSignBytes, algorithm: algorithm.options })
const base64UrlEncodedSignature = Convert.uint8Array(signatureBytes).toBase64Url()

if (detached) {
Expand Down Expand Up @@ -170,13 +169,14 @@ export class Crypto {
throw new Error('Signature verification failed: Expected JWS header to contain alg and kid')
}

const verificationMethod = await deferenceDidUrl(jwsHeader.kid)
const verificationMethod = await deferenceDidUrl(jwsHeader.kid as string)
if (!isVerificationMethod(verificationMethod)) { // ensure that appropriate verification method was found
throw new Error('Signature verification failed: Expected kid in JWS header to dereference to a DID Document Verification Method')
}

// will be used to verify signature
const { publicKeyJwk } = verificationMethod
const publicKeyJwk = verificationMethod.publicKeyJwk as JwkParamsEcPublic | JwkParamsOkpPublic

if (!publicKeyJwk) { // ensure that Verification Method includes public key as a JWK.
throw new Error('Signature verification failed: Expected kid in JWS header to dereference to a DID Document Verification Method with publicKeyJwk')
}
Expand All @@ -186,37 +186,19 @@ export class Crypto {

const signatureBytes = Convert.base64Url(base64UrlEncodedSignature).toUint8Array()

const algorithmId = `${jwsHeader['alg']}:${Crypto.extractNamedCurve(publicKeyJwk)}`
const algorithmId = `${jwsHeader['alg']}:${publicKeyJwk['crv']}`
const { signer, options } = Crypto.algorithms[algorithmId]

// TODO: remove this monkeypatch once 'ext' is no longer a required property within a jwk passed to `jwkToCryptoKey`
const monkeyPatchPublicKeyJwk = {
...publicKeyJwk,
ext : 'true' as const,
key_ops : ['verify']
}

const key = await Jose.jwkToCryptoKey({ key: monkeyPatchPublicKeyJwk })
const isLegit = await signer.verify({ algorithm: options, key, data: signedDataBytes, signature: signatureBytes })
const isLegit = await signer.verify({ algorithm: options, key: publicKeyJwk, data: signedDataBytes, signature: signatureBytes })

if (!isLegit) {
throw new Error('Signature verification failed: Integrity mismatch')
}

const [did] = jwsHeader.kid.split('#')
const [did] = (jwsHeader as JwsHeaderParams).kid.split('#')
return did
}

/**
* Gets crv property from a PublicKeyJwk or PrivateKeyJwk. Returns empty string if crv is undefined.
*/
static extractNamedCurve(jwk: PrivateKeyJwk | PublicKeyJwk | undefined): string {
if (jwk && 'crv' in jwk) {
return jwk.crv
} else {
return ''
}
}
}

/**
Expand Down
90 changes: 3 additions & 87 deletions packages/protocol/src/dev-tools.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@

import type { OfferingData, QuoteData, RfqData } from './types.js'
import type { PortableDid } from '@web5/dids'

import { DidIonMethod, DidKeyMethod } from '@web5/dids'
import { utils as vcUtils } from '@web5/credentials'
import { VerifiableCredential } from '@web5/credentials'
import { Offering } from './resource-kinds/index.js'
import { Convert } from '@web5/common'
import { Crypto } from './crypto.js'
import { Jose } from '@web5/crypto'
import { Rfq } from './message-kinds/index.js'
import { Resource } from './resource.js'

Expand Down Expand Up @@ -219,9 +215,9 @@ export class DevTools {
let credential: any = ''

if (opts?.sender) {
const { signedCredential } = await DevTools.createCredential({
const signedCredential = VerifiableCredential.create({
type : 'YoloCredential',
issuer : opts.sender,
issuer : opts.sender.did,
subject : opts.sender.did,
data : {
'beep': 'boop'
Expand Down Expand Up @@ -251,84 +247,4 @@ export class DevTools {
claims : [credential]
}
}

/**
* creates a verifiable credential using the options provided. This method is intended for testing purposes
* @param opts - options used to create the credential
* @returns
*/
static async createCredential(opts: CreateCredentialOptions) {
const credential = {
'@context' : ['https://www.w3.org/2018/credentials/v1'],
'id' : Date.now().toString(),
'type' : ['VerifiableCredential', opts.type],
'issuer' : opts.issuer.did,
'issuanceDate' : vcUtils.getCurrentXmlSchema112Timestamp(),
'credentialSubject' : { id: opts.subject, ...opts.data }
}

const signedCredential = await DevTools.createJwt({
issuer : opts.issuer,
subject : credential.credentialSubject.id,
payload : { vc: credential }
})

return { credential, signedCredential }
}

/**
* Creates a JWT using the options provided.
* It's signed with the issuer's first verification method private key JWK
*
* @param opts - options used to create the JWT
* @returns a compact JWT
*/
static async createJwt(opts: CreateJwtOptions) {
const { issuer, subject, payload } = opts
const privateKeyJwk = issuer.keySet.verificationMethodKeys?.[0].privateKeyJwk
if (!privateKeyJwk) {
throw Error('Could not get private key JWK from issuer')
}

// build jwt header
const algorithmName = privateKeyJwk['alg'] || ''
let namedCurve = Crypto.extractNamedCurve(privateKeyJwk)
const algorithmId = `${algorithmName}:${namedCurve}`
const algorithm = Crypto.algorithms[algorithmId]
const jwtHeader = { alg: algorithm.alg, kid: issuer.document.verificationMethod?.[0]?.id }
const base64urlEncodedJwtHeader = Convert.object(jwtHeader).toBase64Url()

// build jwt payload
const jwtPayload = { iss: issuer.did, sub: subject, ...payload }
const base64urlEncodedJwtPayload = Convert.object(jwtPayload).toBase64Url()

// build what will be signed
const toSign = `${base64urlEncodedJwtHeader}.${base64urlEncodedJwtPayload}`
const bytesToSign = Convert.string(toSign).toUint8Array()

// select signer based on the provided key's named curve
const { signer, options } = algorithm
const signingKey = await Jose.jwkToCryptoKey({ key: privateKeyJwk })

// generate signature
const signatureBytes = await signer.sign({ key: signingKey, data: bytesToSign, algorithm: options })
const base64UrlEncodedSignature = Convert.uint8Array(signatureBytes).toBase64Url()

return `${base64urlEncodedJwtHeader}.${base64urlEncodedJwtPayload}.${base64UrlEncodedSignature}`
}

/**
* convenience method that can be used to decode a COMPACT JWT
* @param compactJwt - the JWT to decode
* @returns
*/
static decodeJwt(compactJwt: string) {
const [base64urlEncodedJwtHeader, base64urlEncodedJwtPayload, base64urlEncodedSignature] = compactJwt.split('.')

return {
header : Convert.base64Url(base64urlEncodedJwtHeader).toObject(),
payload : Convert.base64Url(base64urlEncodedJwtPayload).toObject(),
base64urlEncodedSignature
}
}
}
8 changes: 4 additions & 4 deletions packages/protocol/src/message-kinds/rfq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Ajv from 'ajv'
*/
export type CreateRfqOptions = {
data: MessageKindModel<'rfq'>
metadata: Omit<MessageMetadata<'rfq'>, 'id' |'kind' | 'createdAt' | 'exchangeId'>
metadata: Omit<MessageMetadata<'rfq'>, 'id' | 'kind' | 'createdAt' | 'exchangeId'>
private?: Record<string, any>
}

Expand Down Expand Up @@ -61,7 +61,7 @@ export class Rfq extends Message<'rfq'> {
* @throws if {@link Rfq.payoutMethod} property `paymentDetails` cannot be validated against the provided offering's payoutMethod requiredPaymentDetails
*/
async verifyOfferingRequirements(offering: Offering | ResourceModel<'offering'>) {
if (offering.metadata.id !== this.offeringId) {
if (offering.metadata.id !== this.offeringId) {
throw new Error(`offering id mismatch. (rfq) ${this.offeringId} !== ${offering.metadata.id} (offering)`)
}

Expand Down Expand Up @@ -146,14 +146,14 @@ export class Rfq extends Message<'rfq'> {
return
}

const credentials = PresentationExchange.selectCredentials(this.claims, offering.data.requiredClaims)
const credentials = PresentationExchange.selectCredentials({ vcJwts: this.claims, presentationDefinition: offering.data.requiredClaims })

if (!credentials.length) {
throw new Error(`claims do not fulfill the offering's requirements`)
}

for (let credential of credentials) {
await VerifiableCredential.verify(credential)
await VerifiableCredential.verify({ vcJwt: credential })
}
}

Expand Down
9 changes: 5 additions & 4 deletions packages/protocol/tests/crypto.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { expect } from 'chai'

import { DevTools, Crypto } from '../src/main.js'
import { DidIonMethod, DidKeyMethod } from '@web5/dids'
import { Convert } from '@web5/common'
import { Crypto } from '../src/main.js'

describe('Crypto', () => {
describe('sign / verify', () => {
it('works with did:ion', async () => {
const alice = await DevTools.createDid('ion')
const alice = await DidIonMethod.create()
const payload = { timestamp: new Date().toISOString() }
const payloadBytes = Convert.object(payload).toUint8Array()

Expand All @@ -15,7 +16,7 @@ describe('Crypto', () => {
}).timeout(30_000)

it('works with did:key', async () => {
const alice = await DevTools.createDid('key')
const alice = await DidKeyMethod.create()

const payload = { timestamp: new Date().toISOString() }
const payloadBytes = Convert.object(payload).toUint8Array()
Expand All @@ -25,7 +26,7 @@ describe('Crypto', () => {
})

it('works with detached content', async () => {
const alice = await DevTools.createDid('ion')
const alice = await DidIonMethod.create()
const payload = { timestamp: new Date().toISOString() }
const payloadBytes = Convert.object(payload).toUint8Array()

Expand Down
Loading

0 comments on commit 7f56d82

Please sign in to comment.