Skip to content

Commit

Permalink
Stricten typescript in protocol package (#141)
Browse files Browse the repository at this point in the history
* Stricten typescript in protocol package

* Add TODO
  • Loading branch information
diehuxx authored Jan 19, 2024
1 parent 64f1747 commit faf4510
Show file tree
Hide file tree
Showing 13 changed files with 84 additions and 44 deletions.
33 changes: 23 additions & 10 deletions packages/protocol/src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import type {
PrivateKeyJwk as Web5PrivateKeyJwk,
CryptoAlgorithm,
Web5Crypto,
JwsHeaderParams
JwsHeaderParams,
PrivateKeyJwk,
PublicKeyJwk
} from '@web5/crypto'

import { sha256 } from '@noble/hashes/sha256'
Expand Down Expand Up @@ -42,7 +44,7 @@ export type VerifyOptions = {
*/
type SignerValue<T extends Web5Crypto.Algorithm> = {
signer: CryptoAlgorithm,
options?: T,
options: T,
alg: JwsHeader['alg'],
crv: JsonWebKey['crv']
}
Expand Down Expand Up @@ -99,18 +101,18 @@ export class Crypto {
static async sign(opts: SignOptions) {
const { did, payload, detached } = opts

const { privateKeyJwk } = did.keySet.verificationMethodKeys[0]
const privateKeyJwk = did.keySet.verificationMethodKeys?.[0]?.privateKeyJwk

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

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

let verificationMethodId = did.document.verificationMethod[0].id
let verificationMethodId = did.document.verificationMethod?.[0]?.id || ''
if (verificationMethodId.startsWith('#')) {
verificationMethodId = `${did.did}#${verificationMethodId}`
}
Expand Down Expand Up @@ -168,7 +170,7 @@ export class Crypto {
throw new Error('Signature verification failed: Expected JWS header to contain alg and kid')
}

const verificationMethod = await deferenceDidUrl(jwsHeader.kid as string)
const verificationMethod = await deferenceDidUrl(jwsHeader.kid)
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')
}
Expand All @@ -184,7 +186,7 @@ export class Crypto {

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

const algorithmId = `${jwsHeader['alg']}:${publicKeyJwk['crv']}`
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`
Expand All @@ -201,9 +203,20 @@ export class Crypto {
throw new Error('Signature verification failed: Integrity mismatch')
}

const [did] = (jwsHeader as JwsHeaderParams).kid.split('#')
const [did] = jwsHeader.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
13 changes: 9 additions & 4 deletions packages/protocol/src/dev-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,12 +285,17 @@ export class DevTools {
*/
static async createJwt(opts: CreateJwtOptions) {
const { issuer, subject, payload } = opts
const { privateKeyJwk } = issuer.keySet.verificationMethodKeys[0]
const privateKeyJwk = issuer.keySet.verificationMethodKeys?.[0].privateKeyJwk
if (!privateKeyJwk) {
throw Error('Could not get private key JWK from issuer')
}

// build jwt header
const algorithmId = `${privateKeyJwk['alg']}:${privateKeyJwk['crv']}`
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 jwtHeader = { alg: algorithm.alg, kid: issuer.document.verificationMethod?.[0]?.id }
const base64urlEncodedJwtHeader = Convert.object(jwtHeader).toBase64Url()

// build jwt payload
Expand All @@ -317,7 +322,7 @@ export class DevTools {
* @param compactJwt - the JWT to decode
* @returns
*/
static decodeJwt(compactJwt) {
static decodeJwt(compactJwt: string) {
const [base64urlEncodedJwtHeader, base64urlEncodedJwtPayload, base64urlEncodedSignature] = compactJwt.split('.')

return {
Expand Down
14 changes: 8 additions & 6 deletions packages/protocol/src/did-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export async function resolveDid(did: string): Promise<DidDocument> {
throw new Error(`Failed to resolve DID: ${did}. Error: ${didResolutionMetadata.error}`)
}

return didDocument
// If did resolution has no errors, assume we have did document
return didDocument!
}

/**
Expand All @@ -35,6 +36,7 @@ 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)
Expand All @@ -47,7 +49,7 @@ export type DidResource = DidDocument | VerificationMethod | DidService
* @throws if DID cannot be resolved
* @beta
*/
export async function deferenceDidUrl(didUrl: string): Promise<DidResource> {
export async function deferenceDidUrl(didUrl: string): Promise<DidResource | undefined> {
const parsedDid = didUtils.parseDid({ didUrl })
if (!parsedDid) {
throw new Error('failed to parse did')
Expand All @@ -67,13 +69,13 @@ export async function deferenceDidUrl(didUrl: string): Promise<DidResource> {
// 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) {
for (let vm of verificationMethod || []) {
if (idSet.has(vm.id)) {
return vm
}
}

for (let svc of service) {
for (let svc of service || []) {
if (idSet.has(svc.id)) {
return svc
}
Expand All @@ -86,6 +88,6 @@ export async function deferenceDidUrl(didUrl: string): Promise<DidResource> {
* @returns true if the didResource is a `VerificationMethod`
* @beta
*/
export function isVerificationMethod(didResource: DidResource): didResource is VerificationMethod {
return didResource && 'id' in didResource && 'type' in didResource && 'controller' in didResource
export function isVerificationMethod(didResource: DidResource | undefined): didResource is VerificationMethod {
return !!didResource && 'id' in didResource && 'type' in didResource && 'controller' in didResource
}
8 changes: 6 additions & 2 deletions packages/protocol/src/message-kinds/rfq.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class Rfq extends Message<'rfq'> {
readonly validNext = new Set<MessageKind>(['quote', 'close'])

/** private data (PII or PCI) */
_private: Record<string, any>
_private: Record<string, any> | undefined

/**
* Creates an rfq with the given options
Expand Down Expand Up @@ -141,7 +141,11 @@ export class Rfq extends Message<'rfq'> {
* @param offering - the offering to check against
* @throws if rfq's claims do not fulfill the offering's requirements
*/
async verifyClaims(offering: Offering | ResourceModel<'offering'>) {
async verifyClaims(offering: Offering | ResourceModel<'offering'>): Promise<void> {
if (!offering.data.requiredClaims) {
return
}

const credentials = PresentationExchange.selectCredentials(this.claims, offering.data.requiredClaims)

if (!credentials.length) {
Expand Down
13 changes: 9 additions & 4 deletions packages/protocol/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { PortableDid } from '@web5/dids'
export abstract class Message<T extends MessageKind> {
private _metadata: MessageMetadata<T>
private _data: MessageKindModel<T>
private _signature: string
private _signature: string | undefined

/**
* used by {@link Message.parse} to return an instance of message kind's class. This abstraction is needed
Expand All @@ -41,7 +41,8 @@ export abstract class Message<T extends MessageKind> {
try {
jsonMessage = typeof message === 'string' ? JSON.parse(message): message
} catch(e) {
throw new Error(`parse: Failed to parse message. Error: ${e.message}`)
const errorMessage = e instanceof Error ? e.message : e
throw new Error(`parse: Failed to parse message. Error: ${errorMessage}`)
}

await Message.verify(jsonMessage)
Expand All @@ -53,14 +54,16 @@ export abstract class Message<T extends MessageKind> {
* validates the message and verifies the cryptographic signature
* @throws if the message is invalid
* @throws see {@link Crypto.verify}
* @returns Message signer's DID
*/
static async verify<T extends MessageKind>(message: MessageModel<T> | Message<T>): Promise<string> {
let jsonMessage: MessageModel<T> = message instanceof Message ? message.toJSON() : message

Message.validate(jsonMessage)

const digest = Crypto.digest({ metadata: jsonMessage.metadata, data: jsonMessage.data })
const signer = await Crypto.verify({ detachedPayload: digest, signature: jsonMessage.signature })
// Message.validate() guarantees presence of signature
const signer = await Crypto.verify({ detachedPayload: digest, signature: jsonMessage.signature! })

if (jsonMessage.metadata.from !== signer) { // ensure that DID used to sign matches `from` property in metadata
throw new Error('Signature verification failed: Expected DID in kid of JWS header must match metadata.from')
Expand Down Expand Up @@ -122,8 +125,9 @@ export abstract class Message<T extends MessageKind> {
* validates the message and verifies the cryptographic signature
* @throws if the message is invalid
* @throws see {@link Crypto.verify}
* @returns Signer's DID
*/
async verify() {
async verify(): Promise<string> {
return Message.verify(this)
}

Expand Down Expand Up @@ -199,6 +203,7 @@ export abstract class Message<T extends MessageKind> {

/**
* returns the message as a json object. Automatically used by `JSON.stringify` method.
* @throws if message is missing signature
*/
toJSON() {
const message: MessageModel<T> = {
Expand Down
2 changes: 1 addition & 1 deletion packages/protocol/src/resource-kinds/offering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class Offering extends Resource<'offering'> {
/** Articulates the claim(s) required when submitting an RFQ for this offering. */
// TODO: Remove type annotation once type alias replaced with direct export in @web5/credentials
// [Link to the PR](https://github.com/TBD54566975/web5-js/pull/336)
get requiredClaims(): PresentationDefinitionV2 {
get requiredClaims(): PresentationDefinitionV2 | undefined {
return this.data.requiredClaims
}
}
12 changes: 8 additions & 4 deletions packages/protocol/src/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { PortableDid } from '@web5/dids'
export abstract class Resource<T extends ResourceKind> {
private _metadata: ResourceMetadata<T>
private _data: ResourceKindModel<T>
private _signature: string
private _signature: string | undefined

/**
* used by {@link Resource.parse} to return an instance of resource kind's class. This abstraction is needed
Expand Down Expand Up @@ -47,7 +47,8 @@ export abstract class Resource<T extends ResourceKind> {
try {
jsonResource = typeof resource === 'string' ? JSON.parse(resource): resource
} catch(e) {
throw new Error(`parse: Failed to parse resource. Error: ${e.message}`)
const errorMessage = e instanceof Error ? e.message : e
throw new Error(`parse: Failed to parse resource. Error: ${errorMessage}`)
}

await Resource.verify(jsonResource)
Expand All @@ -59,13 +60,15 @@ export abstract class Resource<T extends ResourceKind> {
* validates the resource and verifies the cryptographic signature
* @throws if the message is invalid
* @throws see {@link Crypto.verify}
* @returns Resource signer's DID
*/
static async verify<T extends ResourceKind>(resource: ResourceModel<T> | Resource<T>): Promise<string> {
let jsonResource: ResourceModel<T> = resource instanceof Resource ? resource.toJSON() : resource
Resource.validate(jsonResource)

const digest = Crypto.digest({ metadata: jsonResource.metadata, data: jsonResource.data })
const signerDid = await Crypto.verify({ detachedPayload: digest, signature: jsonResource.signature })
// Resource.validate() guarantees presence of signature
const signerDid = await Crypto.verify({ detachedPayload: digest, signature: jsonResource.signature! })

if (jsonResource.metadata.from !== signerDid) { // ensure that DID used to sign matches `from` property in metadata
throw new Error('Signature verification failed: Expected DID in kid of JWS header must match metadata.from')
Expand Down Expand Up @@ -116,8 +119,9 @@ export abstract class Resource<T extends ResourceKind> {
* validates the resource and verifies the cryptographic signature
* @throws if the resource is invalid
* @throws see {@link Crypto.verify}
* @returns Resource signer's DID
*/
async verify() {
async verify(): Promise<string> {
return Resource.verify(this)
}

Expand Down
6 changes: 4 additions & 2 deletions packages/protocol/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export type ResourceModel<T extends ResourceKind> = {
/** The actual resource content */
data: ResourceKindModel<T>
/** signature that verifies that authenticity and integrity of a message */
signature: string
signature?: string
}

/**
Expand Down Expand Up @@ -121,8 +121,10 @@ export type MessageModel<T extends MessageKind> = {
metadata: MessageMetadata<T>
/** The actual message content */
data: MessageKindModel<T>
/** Private data that must not be in the main */
private?: T extends 'rfq' ? Record<string, any> : never
/** signature that verifies that authenticity and integrity of a message */
signature: string
signature?: string
}

/**
Expand Down
4 changes: 4 additions & 0 deletions packages/protocol/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export function messageFactory<T extends MessageKind>(jsonMessage: MessageModel<
case 'order': return new Order(jsonMessage as NewMessage<'order'>)
case 'orderstatus': return new OrderStatus(jsonMessage as NewMessage<'orderstatus'>)
case 'close': return new Close(jsonMessage as NewMessage<'close'>)
default:
throw new Error(`Unrecognized message kind (${jsonMessage.metadata.kind})`)
}
}

Expand All @@ -36,5 +38,7 @@ export function messageFactory<T extends MessageKind>(jsonMessage: MessageModel<
export function resourceFactory<T extends ResourceKind>(jsonResource: ResourceModel<T>): ResourceKindClass {
switch(jsonResource.metadata.kind) {
case 'offering': return new Offering(jsonResource as NewResource<'offering'>)
default:
throw new Error(`Unrecognized resource kind (${jsonResource.metadata.kind})`)
}
}
13 changes: 6 additions & 7 deletions packages/protocol/tests/offering.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ describe('Offering', () => {
const pfi = await DevTools.createDid()

try {
const offeringData = DevTools.createOfferingData()
offeringData['foo'] = 'bar'
const offeringData = DevTools.createOfferingData();
(offeringData as any)['foo'] = 'bar'
const offering = Offering.create({
metadata : { from: pfi.did },
data : offeringData
Expand Down Expand Up @@ -81,14 +81,13 @@ describe('Offering', () => {
data : DevTools.createOfferingData()
})


await offering.sign(pfi)

const [base64UrlEncodedJwsHeader] = offering.signature.split('.')
const jwsHeader = Convert.base64Url(base64UrlEncodedJwsHeader).toObject()
const [base64UrlEncodedJwsHeader] = offering.signature!.split('.')
const jwsHeader: { kid?: string, alg?: string} = Convert.base64Url(base64UrlEncodedJwsHeader).toObject()

expect(jwsHeader['kid']).to.equal(pfi.document.verificationMethod[0].id)
expect(jwsHeader['alg']).to.exist
expect(jwsHeader.kid).to.equal(pfi.document.verificationMethod![0].id)
expect(jwsHeader.alg).to.exist
})
})

Expand Down
6 changes: 3 additions & 3 deletions packages/protocol/tests/rfq.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ describe('Rfq', () => {

await rfq.sign(did)

const [base64UrlEncodedJwsHeader] = rfq.signature.split('.')
const jwsHeader = Convert.base64Url(base64UrlEncodedJwsHeader).toObject()
const [base64UrlEncodedJwsHeader] = rfq.signature!.split('.')
const jwsHeader: { kid?: string, alg?: string} = Convert.base64Url(base64UrlEncodedJwsHeader).toObject()

expect(jwsHeader['kid']).to.equal(did.document.verificationMethod[0].id)
expect(jwsHeader['kid']).to.equal(did.document.verificationMethod![0].id)
expect(jwsHeader['alg']).to.exist
})
})
Expand Down
2 changes: 2 additions & 0 deletions packages/protocol/tests/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"strict": true,
"useUnknownInCatchVariables": false,
"outDir": "compiled",
"declarationDir": "compiled/types",
"sourceMap": true,
Expand Down
Loading

0 comments on commit faf4510

Please sign in to comment.