Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update web5/dids, web5/credentials, web5/crypto, web5/common to latest #177

Merged
merged 8 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/breezy-islands-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@tbdex/http-client": minor
"@tbdex/http-server": minor
"@tbdex/protocol": minor
---

Upgrade packages web5/[email protected], web5/[email protected], web5/[email protected], web5/[email protected]
KendallWeihe marked this conversation as resolved.
Show resolved Hide resolved

Deprecate did:ion and did:key in favour of did:jwk and did:dht
8 changes: 4 additions & 4 deletions packages/http-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@
},
"dependencies": {
"@tbdex/protocol": "workspace:*",
"@web5/common": "0.2.2",
"@web5/credentials": "0.4.1",
"@web5/crypto": "0.2.4",
"@web5/dids": "0.2.4",
"@web5/common": "0.2.3",
"@web5/credentials": "0.4.2",
"@web5/crypto": "0.4.0",
"@web5/dids": "0.4.0",
"ms": "2.1.3",
"query-string": "8.1.0",
"typeid-js": "0.3.0"
Expand Down
15 changes: 8 additions & 7 deletions packages/http-client/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { JwtPayload } from '@web5/crypto'
import type { ErrorDetail } from './types.js'
import type { DidDocument, PortableDid } from '@web5/dids'
import type { DidDocument, BearerDid } from '@web5/dids'
import {
MessageModel,
Parser,
Expand Down Expand Up @@ -30,7 +30,7 @@ import ms from 'ms'
* @beta
*/
export type GenerateRequestTokenParams = {
requesterDid: PortableDid
requesterDid: BearerDid
pfiDid: string
}

Expand Down Expand Up @@ -251,19 +251,20 @@ export class TbdexHttpClient {
* @throws {@link RequestTokenSigningError} If an error occurs during the token generation.
*/
static async generateRequestToken(params: GenerateRequestTokenParams): Promise<string> {
const { pfiDid, requesterDid } = params
const now = Date.now()
const exp = (now + ms('1m'))

const jwtPayload: JwtPayload = {
aud : params.pfiDid,
iss : params.requesterDid.did,
aud : pfiDid,
iss : requesterDid.uri,
exp : Math.floor(exp / 1000),
iat : Math.floor(now / 1000),
jti : typeid().getSuffix()
}

try {
return await Jwt.sign({ signerDid: params.requesterDid, payload: jwtPayload })
return await Jwt.sign({ signerDid: requesterDid, payload: jwtPayload })
} catch(e) {
throw new RequestTokenSigningError({ message: e.message, cause: e })
}
Expand Down Expand Up @@ -351,7 +352,7 @@ export type GetExchangeOptions = {
/** the exchange you want to fetch */
exchangeId: string
/** the message author's DID */
did: PortableDid
did: BearerDid
}

/**
Expand All @@ -361,7 +362,7 @@ export type GetExchangeOptions = {
export type GetExchangesOptions = {
/** the DID of the PFI from whom you want to get offerings */
pfiDid: string
did: PortableDid,
did: BearerDid,
filter?: {
id: string | string[]
}
Expand Down
112 changes: 54 additions & 58 deletions packages/http-client/tests/client.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai'
import { DidDhtMethod, DidKeyMethod, PortableDid } from '@web5/dids'
import { DidDht, DidJwk, BearerDid } from '@web5/dids'
import { TbdexHttpClient, requestTokenRequiredClaims } from '../src/client.js'
import {
RequestError,ResponseError,
Expand All @@ -12,19 +12,22 @@ import {
} from '../src/errors/index.js'
import { DevTools } from '@tbdex/protocol'
import * as sinon from 'sinon'
import { JwtHeaderParams, JwtPayload, PrivateKeyJwk, Secp256k1 } from '@web5/crypto'
import { JwtHeaderParams, JwtPayload } from '@web5/crypto'
import { Convert } from '@web5/common'
import { Jwt } from '@web5/credentials'

const dhtDid = await DidDhtMethod.create({
publish : true,
services : [{
type : 'PFI',
id : 'pfi',
serviceEndpoint : 'https://localhost:9000'
}]
const pfiDid: BearerDid = await DidDht.create({
options: {
services: [{
type : 'PFI',
id : 'pfi',
serviceEndpoint : 'https://localhost:9000'
}]
}
})

const aliceDid: BearerDid = await DidJwk.create()

// TODO : Instead of stubbing fetch, consider using libraries like msw
const fetchStub = sinon.stub(globalThis, 'fetch')
const getPfiServiceEndpointStub = sinon.stub(TbdexHttpClient, 'getPfiServiceEndpoint')
Expand All @@ -33,13 +36,6 @@ describe('client', () => {
beforeEach(() => getPfiServiceEndpointStub.resolves('https://localhost:9000'))

describe('sendMessage', () => {
let aliceDid: PortableDid
let pfiDid: PortableDid

beforeEach(async () => {
aliceDid = await DevTools.createDid()
pfiDid = await DevTools.createDid()
})

it('throws RequestError if service endpoint url is garbage', async () => {
getPfiServiceEndpointStub.resolves('garbage')
Expand Down Expand Up @@ -80,7 +76,7 @@ describe('client', () => {
expect(e).to.be.instanceof(ResponseError)
expect(e.statusCode).to.exist
expect(e.details).to.exist
expect(e.recipientDid).to.equal(pfiDid.did)
expect(e.recipientDid).to.equal(pfiDid.uri)
expect(e.url).to.equal(`https://localhost:9000/exchanges/${rfq.metadata.exchangeId}/rfq`)
}
})
Expand Down Expand Up @@ -124,7 +120,7 @@ describe('client', () => {
fetchStub.rejects({message: 'Failed to fetch on URL'})

try {
await TbdexHttpClient.getOfferings({ pfiDid: dhtDid.did })
await TbdexHttpClient.getOfferings({ pfiDid: pfiDid.uri })
expect.fail()
} catch(e) {
expect(e.name).to.equal('RequestError')
Expand All @@ -145,14 +141,14 @@ describe('client', () => {
} as Response)

try {
await TbdexHttpClient.getOfferings({ pfiDid: dhtDid.did })
await TbdexHttpClient.getOfferings({ pfiDid: pfiDid.uri })
expect.fail()
} catch(e) {
expect(e.name).to.equal('ResponseError')
expect(e).to.be.instanceof(ResponseError)
expect(e.statusCode).to.exist
expect(e.details).to.exist
expect(e.recipientDid).to.equal(dhtDid.did)
expect(e.recipientDid).to.equal(pfiDid.uri)
expect(e.url).to.equal('https://localhost:9000/offerings')
}
})
Expand All @@ -163,7 +159,7 @@ describe('client', () => {
json : () => Promise.resolve({ data: [] })
} as Response)

const offerings = await TbdexHttpClient.getOfferings({ pfiDid: dhtDid.did })
const offerings = await TbdexHttpClient.getOfferings({ pfiDid: pfiDid.uri })
expect(offerings).to.have.length(0)
})
})
Expand All @@ -174,7 +170,7 @@ describe('client', () => {
fetchStub.rejects({message: 'Failed to fetch on URL'})

try {
await TbdexHttpClient.getExchange({ pfiDid: dhtDid.did, exchangeId: '123', did: dhtDid })
await TbdexHttpClient.getExchange({ pfiDid: pfiDid.uri, exchangeId: '123', did: pfiDid })
expect.fail()
} catch(e) {
expect(e.name).to.equal('RequestError')
Expand All @@ -195,14 +191,14 @@ describe('client', () => {
} as Response)

try {
await TbdexHttpClient.getExchange({ pfiDid: dhtDid.did, exchangeId: '123', did: dhtDid })
await TbdexHttpClient.getExchange({ pfiDid: pfiDid.uri, exchangeId: '123', did: pfiDid })
expect.fail()
} catch(e) {
expect(e.name).to.equal('ResponseError')
expect(e).to.be.instanceof(ResponseError)
expect(e.statusCode).to.exist
expect(e.details).to.exist
expect(e.recipientDid).to.equal(dhtDid.did)
expect(e.recipientDid).to.equal(pfiDid.uri)
expect(e.url).to.equal('https://localhost:9000/exchanges/123')
}
})
Expand All @@ -213,7 +209,7 @@ describe('client', () => {
json : () => Promise.resolve({ data: [] })
} as Response)

const exchanges = await TbdexHttpClient.getExchange({ pfiDid: dhtDid.did, exchangeId: '123', did: dhtDid })
const exchanges = await TbdexHttpClient.getExchange({ pfiDid: pfiDid.uri, exchangeId: '123', did: pfiDid })
expect(exchanges).to.have.length(0)
})
})
Expand All @@ -224,7 +220,7 @@ describe('client', () => {
fetchStub.rejects({message: 'Failed to fetch on URL'})

try {
await TbdexHttpClient.getExchanges({ pfiDid: dhtDid.did, did: dhtDid })
await TbdexHttpClient.getExchanges({ pfiDid: pfiDid.uri, did: pfiDid })
expect.fail()
} catch(e) {
expect(e.name).to.equal('RequestError')
Expand All @@ -245,14 +241,14 @@ describe('client', () => {
} as Response)

try {
await TbdexHttpClient.getExchanges({ pfiDid: dhtDid.did, did: dhtDid })
await TbdexHttpClient.getExchanges({ pfiDid: pfiDid.uri, did: pfiDid })
expect.fail()
} catch(e) {
expect(e.name).to.equal('ResponseError')
expect(e).to.be.instanceof(ResponseError)
expect(e.statusCode).to.exist
expect(e.details).to.exist
expect(e.recipientDid).to.equal(dhtDid.did)
expect(e.recipientDid).to.equal(pfiDid.uri)
expect(e.url).to.equal('https://localhost:9000/exchanges')
}
})
Expand All @@ -263,7 +259,7 @@ describe('client', () => {
json : () => Promise.resolve({ data: [] })
} as Response)

const exchanges = await TbdexHttpClient.getExchanges({ pfiDid: dhtDid.did, did: dhtDid })
const exchanges = await TbdexHttpClient.getExchanges({ pfiDid: pfiDid.uri, did: pfiDid })
expect(exchanges).to.have.length(0)
})
})
Expand All @@ -285,10 +281,8 @@ describe('client', () => {
}
})
it('throws MissingServiceEndpointError if did has no PFI service endpoint', async () => {
const keyDid = await DidKeyMethod.create()

try {
await TbdexHttpClient.getPfiServiceEndpoint(keyDid.did)
await TbdexHttpClient.getPfiServiceEndpoint(aliceDid.uri)
expect.fail()
} catch(e) {
expect(e.name).to.equal('MissingServiceEndpointError')
Expand All @@ -297,73 +291,75 @@ describe('client', () => {
}
})
it('returns pfi service endpoint if all is well', async () => {
const serviceEndpoint = await TbdexHttpClient.getPfiServiceEndpoint(dhtDid.did)
const serviceEndpoint = await TbdexHttpClient.getPfiServiceEndpoint(pfiDid.uri)
expect(serviceEndpoint).to.equal('https://localhost:9000')
})
})

describe('generateRequestToken', () => {
let requesterPortableDid: PortableDid
let requesterBearerDid: BearerDid
before(async () => {
requesterPortableDid = await DidKeyMethod.create({ keyAlgorithm: 'secp256k1' })
})
it('throws a RequestTokenSigningError if requesterDid is not a valid PortableDid', async () => {
try {
await TbdexHttpClient.generateRequestToken({ requesterDid: {did: '', document: { id: '' }, keySet: {}}, pfiDid: '' })
expect.fail()
} catch (e) {
expect(e).to.be.instanceOf(RequestTokenSigningError)
}
requesterBearerDid = await DidJwk.create()
})
it('includes all expected claims', async () => {
const requestToken = await TbdexHttpClient.generateRequestToken({ requesterDid: requesterPortableDid, pfiDid: 'did:key:1234' })
const requestToken = await TbdexHttpClient.generateRequestToken({ requesterDid: requesterBearerDid, pfiDid: 'did:key:1234' })
const decodedToken = await Jwt.verify({ jwt: requestToken })
expect(decodedToken.payload).to.have.all.keys(requestTokenRequiredClaims)
})
// TODO: decide if we want to ensure that the expiration date is not longer than 1 minute after the issuance date
it('sets expiration seconds to 1 minute after the time at which it was issued', async () => {
const requestToken = await TbdexHttpClient.generateRequestToken({ requesterDid: requesterPortableDid, pfiDid: 'did:key:1234' })
const requestToken = await TbdexHttpClient.generateRequestToken({ requesterDid: requesterBearerDid, pfiDid: 'did:key:1234' })
const decodedToken = await Jwt.verify({ jwt: requestToken })
expect(decodedToken.payload.exp! - decodedToken.payload.iat!).to.equal(60)
})
})

describe('verifyRequestToken', () => {
let pfiPortableDid: PortableDid
let header: JwtHeaderParams
let payload: JwtPayload

/*
** helper function to help alice generate a valid request token to send to a pfi
*/
async function createRequestTokenFromPayload(payload: JwtPayload) {
kirahsapong marked this conversation as resolved.
Show resolved Hide resolved
const privateKeyJwk = pfiPortableDid.keySet.verificationMethodKeys![0].privateKeyJwk
const signer = await pfiDid.getSigner()
header = { typ: 'JWT', alg: signer.algorithm, kid: signer.keyId }
const base64UrlEncodedHeader = Convert.object(header).toBase64Url()
const base64UrlEncodedPayload = Convert.object(payload).toBase64Url()

const toSign = `${base64UrlEncodedHeader}.${base64UrlEncodedPayload}`
const toSignBytes = Convert.string(toSign).toUint8Array()
const signatureBytes = await Secp256k1.sign({ key: privateKeyJwk as PrivateKeyJwk, data: toSignBytes })
const signatureBytes = await signer.sign({ data: toSignBytes })
const base64UrlEncodedSignature = Convert.uint8Array(signatureBytes).toBase64Url()

return `${toSign}.${base64UrlEncodedSignature}`
}

before(async () => {
pfiPortableDid = await DidKeyMethod.create({ keyAlgorithm: 'secp256k1' })
header = { typ: 'JWT', alg: 'ES256K', kid: pfiPortableDid.document.verificationMethod![0].id }
})

beforeEach(() => {
payload = {
iat : Math.floor(Date.now() / 1000),
aud : pfiPortableDid.did,
aud : pfiDid.uri,
iss : 'did:key:1234',
exp : Math.floor(Date.now() / 1000 + 60),
jti : 'randomnonce'
}
})

it('throws a RequestTokenSigningError if token cannot be signed', async () => {
const jwtSigner = sinon.stub(Jwt, 'sign')
jwtSigner.throws()
try {
await TbdexHttpClient.generateRequestToken({ requesterDid: aliceDid, pfiDid: ''})
expect.fail()
} catch (e) {
expect(e).to.be.instanceOf(RequestTokenSigningError)
}
jwtSigner.restore()
})

it('throws RequestTokenVerificationError if request token is not a valid jwt', async () => {
try {
await TbdexHttpClient.verifyRequestToken({ requestToken: '', pfiDid: pfiPortableDid.did })
await TbdexHttpClient.verifyRequestToken({ requestToken: '', pfiDid: pfiDid.uri })
expect.fail()
} catch(e) {
expect(e).to.be.instanceof(RequestTokenVerificationError)
Expand All @@ -375,7 +371,7 @@ describe('client', () => {
try {
delete payload[claim]
const requestToken = await createRequestTokenFromPayload(payload)
await TbdexHttpClient.verifyRequestToken({ requestToken, pfiDid: pfiPortableDid.did })
await TbdexHttpClient.verifyRequestToken({ requestToken, pfiDid: pfiDid.uri })
expect.fail()
} catch(e) {
expect(e).to.be.instanceof(RequestTokenMissingClaimsError)
Expand All @@ -388,7 +384,7 @@ describe('client', () => {
try {
payload.aud = 'squirtle'
const requestToken = await createRequestTokenFromPayload(payload)
await TbdexHttpClient.verifyRequestToken({ requestToken, pfiDid: pfiPortableDid.did })
await TbdexHttpClient.verifyRequestToken({ requestToken, pfiDid: pfiDid.uri })
expect.fail()
} catch(e) {
expect(e).to.be.instanceof(RequestTokenAudienceMismatchError)
Expand All @@ -397,7 +393,7 @@ describe('client', () => {
})
it('returns requester\'s DID if request token is valid', async () => {
const requestToken = await createRequestTokenFromPayload(payload)
const iss = await TbdexHttpClient.verifyRequestToken({ requestToken, pfiDid: pfiPortableDid.did })
const iss = await TbdexHttpClient.verifyRequestToken({ requestToken, pfiDid: pfiDid.uri })
expect(iss).to.equal('did:key:1234')
})
})
Expand Down
2 changes: 1 addition & 1 deletion packages/http-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"dependencies": {
"@tbdex/http-client": "workspace:*",
"@tbdex/protocol": "workspace:*",
"@web5/dids": "0.2.2",
"@web5/dids": "0.4.0",
"cors": "2.8.5",
"express": "4.18.2"
},
Expand Down
Loading
Loading