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

bumping web5 versions #149

Merged
merged 9 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions .changeset/real-schools-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@tbdex/http-client": minor
"@tbdex/protocol": minor
---

Upgrading web5 versions in protocol and http-client
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"
Comment on lines +57 to +59
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding ms and typeid-js to http-client in this PR? If ms is necessary in dependencies, why is @types/ms in devDepencencies?

Copy link
Contributor Author

@jiyoontbd jiyoontbd Jan 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding ms and typeid-js to http-client in this PR?

so this PR is part of a 4 PR series in an effort to break down moe's 122-request-token PR which is a chonky boi.

i just ported over the client/package.json changes from the original PR, which includes ms and typeid-js since they're being used by the client during request token generation.

the PR that will use the ms and typeid-js is here

If ms is necessary in dependencies, why is @types/ms in devDepencencies?

i thought we needed to include @types/xyz in devDependencies that correspond to package xyz in dependencies in order to write with type checking during development. is that incorrect?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i thought we needed to include @types/xyz in devDependencies that correspond to package xyz in dependencies in order to write with type checking during development. is that incorrect?

ah, u right

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yee that's correct @jiyoontbd

},
"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.1",
"@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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should sinon and @types/sinon have matching versions? Only different by patch version so probably not a big deal regardless

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, ty!

"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
38 changes: 17 additions & 21 deletions packages/protocol/src/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import type {
PrivateKeyJwk as Web5PrivateKeyJwk,
CryptoAlgorithm,
Web5Crypto,
JwsHeaderParams,
JwkParamsEcPrivate,
JwkParamsOkpPrivate,
JwkParamsEcPublic,
JwkParamsOkpPublic,
PrivateKeyJwk,
PublicKeyJwk
} from '@web5/crypto'

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

import canonicalize from 'canonicalize'
import { PortableDid } from '@web5/dids'
import { PortableDid, VerificationMethod } from '@web5/dids'

/**
* Options passed to {@link Crypto.sign}
Expand Down Expand Up @@ -51,7 +54,7 @@ type SignerValue<T extends Web5Crypto.Algorithm> = {

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,7 +104,7 @@ 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)
Expand All @@ -114,19 +117,17 @@ export class Crypto {

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 +171,16 @@ export class Crypto {
throw new Error('Signature verification failed: Expected JWS header to contain alg and kid')
}

const verificationMethod = await deferenceDidUrl(jwsHeader.kid)
const dereferenceResult = await DidResolver.dereference({ didUrl: jwsHeader.kid })

const verificationMethod = dereferenceResult.contentStream as VerificationMethod
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 @@ -189,15 +193,7 @@ export class Crypto {
const algorithmId = `${jwsHeader['alg']}:${Crypto.extractNamedCurve(publicKeyJwk)}`
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')
Expand Down
98 changes: 8 additions & 90 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 { DidDhtMethod, DidIonMethod, DidKeyMethod } from '@web5/dids'
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 @@ -38,7 +34,7 @@
* Options passed to {@link DevTools.createCredential}
* @beta
*/
export type CreateCredentialOptions = Omit<CreateJwtOptions, 'payload'> & {

Check warning on line 37 in packages/protocol/src/dev-tools.ts

View workflow job for this annotation

GitHub Actions / tbdocs-reporter

extractor: ae-unresolved-link

The `@link` reference could not be resolved: No member was found with name "createCredential"
/** the credential type (e.g. UniversityDegreeCredential) */
type: string
/** data to include in the credential */
Expand All @@ -49,7 +45,7 @@
* Options passed to {@link DevTools.createJwt}
* @beta
*/
export type CreateJwtOptions = {

Check warning on line 48 in packages/protocol/src/dev-tools.ts

View workflow job for this annotation

GitHub Actions / tbdocs-reporter

extractor: ae-unresolved-link

The `@link` reference could not be resolved: No member was found with name "createJwt"
/** the thing to sign */
payload: any,
/** the JWT's subject (e.g. Alice's DID) */
Expand All @@ -69,9 +65,11 @@
*/
static async createDid(didMethod: DidMethodOptions = 'key') {
if (didMethod === 'key') {
return DidKeyMethod.create()
return await DidKeyMethod.create()
} else if (didMethod === 'ion') {
return DidIonMethod.create()
return await DidIonMethod.create()
} else if (didMethod === 'dht') {
return await DidDhtMethod.create()
} else {
throw new Error(`${didMethod} method not implemented.`)
}
Expand Down Expand Up @@ -219,9 +217,9 @@
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 +249,4 @@
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
}
}
}
50 changes: 2 additions & 48 deletions packages/protocol/src/did-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { DidDocument, DidService, VerificationMethod } from '@web5/dids'

import { DidResolver as Web5DidResolver, DidKeyMethod, DidIonMethod, DidDhtMethod, utils as didUtils } from '@web5/dids'
import { DidResolver as Web5DidResolver, DidKeyMethod, DidIonMethod, DidDhtMethod } from '@web5/dids'

/**
* Can be used to resolve did:ion and did:key DIDs
Expand Down Expand Up @@ -36,58 +36,12 @@ export async function resolveDid(did: string): Promise<DidDocument> {
*/
export type DidResource = DidDocument | VerificationMethod | DidService

// TODO https://github.com/TBD54566975/tbdex-js/issues/147 Remove deferenceDidUrl with web5/dids DidResolver#dereference
/**
* Dereferences a DID URL according to [specification](https://www.w3.org/TR/did-core/#did-url-dereferencing).
* See also: [DID URL Syntax](https://www.w3.org/TR/did-core/#did-url-syntax)
*
* **Note**: Support is limited to did#fragment within [Verification Method](https://www.w3.org/TR/did-core/#verification-methods)
* and [Service](https://www.w3.org/TR/did-core/#services) only
* @param didUrl - the did url to dereference
* @returns the dereferenced resource
* @throws if DID URL cannot be parsed
* @throws if DID cannot be resolved
* @beta
*/
export async function deferenceDidUrl(didUrl: string): Promise<DidResource | undefined> {
const parsedDid = didUtils.parseDid({ didUrl })
if (!parsedDid) {
throw new Error('failed to parse did')
}

const didDocument = await resolveDid(didUrl)

// return the entire DID Document if no fragment is present on the did url
if (!parsedDid.fragment) {
return didDocument
}

const { service, verificationMethod } = didDocument

// create a set of possible id matches. the DID spec allows for an id to be the entire did#fragment or just #fragment.
// See: https://www.w3.org/TR/did-core/#relative-did-urls
// using a set for fast string comparison. DIDs can be lonnng.
const idSet = new Set([didUrl, parsedDid.fragment, `#${parsedDid.fragment}`])

for (let vm of verificationMethod || []) {
if (idSet.has(vm.id)) {
return vm
}
}

for (let svc of service || []) {
if (idSet.has(svc.id)) {
return svc
}
}
}

/**
* type guard for {@link @web5/dids#VerificationMethod}
* @param didResource - the resource to check
* @returns true if the didResource is a `VerificationMethod`
* @beta
*/
export function isVerificationMethod(didResource: DidResource | undefined): didResource is VerificationMethod {
export function isVerificationMethod(didResource: DidResource | null): didResource is VerificationMethod {
return !!didResource && 'id' in didResource && 'type' in didResource && 'controller' in didResource
}
Loading
Loading