From 1124707caf2f6ed9f7ddae777898973f9107b749 Mon Sep 17 00:00:00 2001 From: dragosp1011 <109967337+dragosp1011@users.noreply.github.com> Date: Tue, 13 Dec 2022 19:12:11 +0200 Subject: [PATCH 01/11] feat(open-payments): add get quote route (#829) * feat(open-payments): add get quote route * fix(open-payments): improve code structure * fix: change api --- packages/open-payments/src/client/index.ts | 7 +++++ .../open-payments/src/client/quote.test.ts | 27 +++++++++++++++++++ packages/open-payments/src/client/quote.ts | 26 ++++++++++++++++++ packages/open-payments/src/types.ts | 1 + 4 files changed, 61 insertions(+) create mode 100644 packages/open-payments/src/client/quote.test.ts create mode 100644 packages/open-payments/src/client/quote.ts diff --git a/packages/open-payments/src/client/index.ts b/packages/open-payments/src/client/index.ts index 5a7408e504..106f8ed0e8 100644 --- a/packages/open-payments/src/client/index.ts +++ b/packages/open-payments/src/client/index.ts @@ -21,6 +21,7 @@ import { createOutgoingPaymentRoutes, OutgoingPaymentRoutes } from './outgoing-payment' +import { createQuoteRoutes, QuoteRoutes } from './quote' export interface BaseDeps { axiosInstance: AxiosInstance @@ -104,6 +105,7 @@ export interface AuthenticatedClient extends UnauthenticatedClient { incomingPayment: IncomingPaymentRoutes outgoingPayment: OutgoingPaymentRoutes grant: GrantRoutes + quote: QuoteRoutes } export const createAuthenticatedClient = async ( @@ -138,6 +140,11 @@ export const createAuthenticatedClient = async ( openApi: authServerOpenApi, logger, client: args.paymentPointerUrl + }), + quote: createQuoteRoutes({ + axiosInstance, + openApi: resourceServerOpenApi, + logger }) } } diff --git a/packages/open-payments/src/client/quote.test.ts b/packages/open-payments/src/client/quote.test.ts new file mode 100644 index 0000000000..16c7a978a7 --- /dev/null +++ b/packages/open-payments/src/client/quote.test.ts @@ -0,0 +1,27 @@ +import { createQuoteRoutes } from './quote' +import { OpenAPI, HttpMethod, createOpenAPI } from 'openapi' +import config from '../config' +import { defaultAxiosInstance, silentLogger } from '../test/helpers' + +describe('quote', (): void => { + let openApi: OpenAPI + + beforeAll(async () => { + openApi = await createOpenAPI(config.OPEN_PAYMENTS_RS_OPEN_API_URL) + }) + + const axiosInstance = defaultAxiosInstance + const logger = silentLogger + + describe('createQuoteRoutes', (): void => { + test('calls createResponseValidator properly', async (): Promise => { + jest.spyOn(openApi, 'createResponseValidator') + + createQuoteRoutes({ axiosInstance, openApi, logger }) + expect(openApi.createResponseValidator).toHaveBeenCalledWith({ + path: '/quotes/{id}', + method: HttpMethod.GET + }) + }) + }) +}) diff --git a/packages/open-payments/src/client/quote.ts b/packages/open-payments/src/client/quote.ts new file mode 100644 index 0000000000..faead27027 --- /dev/null +++ b/packages/open-payments/src/client/quote.ts @@ -0,0 +1,26 @@ +import { HttpMethod } from 'openapi' +import { RouteDeps } from '.' +import { getRSPath, Quote } from '../types' +import { get } from './requests' + +interface GetArgs { + url: string +} + +export interface QuoteRoutes { + get(args: GetArgs): Promise +} + +export const createQuoteRoutes = (deps: RouteDeps): QuoteRoutes => { + const { axiosInstance, openApi, logger } = deps + + const getQuoteValidator = openApi.createResponseValidator({ + path: getRSPath('/quotes/{id}'), + method: HttpMethod.GET + }) + + return { + get: (args: GetArgs) => + get({ axiosInstance, logger }, args, getQuoteValidator) + } +} diff --git a/packages/open-payments/src/types.ts b/packages/open-payments/src/types.ts index d757360e94..9debd897bb 100644 --- a/packages/open-payments/src/types.ts +++ b/packages/open-payments/src/types.ts @@ -23,6 +23,7 @@ export type CreateOutgoingPaymentArgs = export type PaymentPointer = RSComponents['schemas']['payment-pointer'] export type JWK = RSComponents['schemas']['json-web-key'] export type JWKS = RSComponents['schemas']['json-web-key-set'] +export type Quote = RSComponents['schemas']['quote'] export const getASPath =

(path: P): string => path as string From 30096d1b3021a9623f3668ede4d0d5ab32a30bf7 Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Wed, 14 Dec 2022 11:01:41 +0100 Subject: [PATCH 02/11] (open-payments): list outgoing payments (#823) * feat(open-payments): first draft of list outgoing payments * chore(open-payments): merge main * feat(open-payments): add proper pagination params * feat(open-payments): adding listOutgoingPayment tests * feat(open-payments): update list method call * feat(open-payments): add tests for query params * chore(open-payments): remove strict type checking for now * feat(open-payments): don't provide queryParams if empty * feat(open-payments): address feedback * feat(open-payments): add test * chore(open-payments): fix test type --- .../src/client/outgoing-payment.test.ts | 171 +++++++++++++++++- .../src/client/outgoing-payment.ts | 71 +++++++- .../open-payments/src/client/requests.test.ts | 40 ++++ packages/open-payments/src/client/requests.ts | 7 +- packages/open-payments/src/test/helpers.ts | 23 ++- packages/open-payments/src/types.ts | 14 ++ 6 files changed, 320 insertions(+), 6 deletions(-) diff --git a/packages/open-payments/src/client/outgoing-payment.test.ts b/packages/open-payments/src/client/outgoing-payment.test.ts index 087922c900..85d869e94f 100644 --- a/packages/open-payments/src/client/outgoing-payment.test.ts +++ b/packages/open-payments/src/client/outgoing-payment.test.ts @@ -2,6 +2,7 @@ import { createOutgoingPayment, createOutgoingPaymentRoutes, getOutgoingPayment, + listOutgoingPayments, validateOutgoingPayment } from './outgoing-payment' import { OpenAPI, HttpMethod, createOpenAPI } from 'openapi' @@ -10,7 +11,8 @@ import { defaultAxiosInstance, mockOutgoingPayment, mockOpenApiResponseValidators, - silentLogger + silentLogger, + mockOutgoingPaymentPaginationResult } from '../test/helpers' import nock from 'nock' import { v4 as uuid } from 'uuid' @@ -42,6 +44,20 @@ describe('outgoing-payment', (): void => { }) }) + test('creates listOutgoingPaymentOpenApiValidator properly', async (): Promise => { + jest.spyOn(openApi, 'createResponseValidator') + + createOutgoingPaymentRoutes({ + axiosInstance, + openApi, + logger + }) + expect(openApi.createResponseValidator).toHaveBeenCalledWith({ + path: '/outgoing-payments', + method: HttpMethod.GET + }) + }) + test('creates createOutgoingPaymentOpenApiValidator properly', async (): Promise => { jest.spyOn(openApi, 'createResponseValidator') @@ -91,7 +107,7 @@ describe('outgoing-payment', (): void => { } }) - nock(baseUrl).get('/outgoing-payment').reply(200, outgoingPayment) + nock(baseUrl).get('/outgoing-payments').reply(200, outgoingPayment) await expect(() => getOutgoingPayment( @@ -111,7 +127,7 @@ describe('outgoing-payment', (): void => { test('throws if outgoing payment does not pass open api validation', async (): Promise => { const outgoingPayment = mockOutgoingPayment() - nock(baseUrl).get('/outgoing-payment').reply(200, outgoingPayment) + nock(baseUrl).get('/outgoing-payments').reply(200, outgoingPayment) await expect(() => getOutgoingPayment( @@ -129,6 +145,155 @@ describe('outgoing-payment', (): void => { }) }) + describe('listOutgoingPayment', (): void => { + const paymentPointer = 'http://localhost:1000/.well-known/pay' + + describe('forward pagination', (): void => { + test.each` + first | cursor + ${undefined} | ${undefined} + ${1} | ${undefined} + ${5} | ${uuid()} + `( + 'returns outgoing payment list', + async ({ first, cursor }): Promise => { + const outgoingPaymentPaginationResult = + mockOutgoingPaymentPaginationResult({ + result: Array(first).fill(mockOutgoingPayment()) + }) + + const scope = nock(paymentPointer) + .get('/outgoing-payments') + .query({ + ...(first ? { first } : {}), + ...(cursor ? { cursor } : {}) + }) + .reply(200, outgoingPaymentPaginationResult) + + const result = await listOutgoingPayments( + { + axiosInstance, + logger + }, + { + paymentPointer, + accessToken: 'accessToken' + }, + openApiValidators.successfulValidator, + { + first, + cursor + } + ) + expect(result).toStrictEqual(outgoingPaymentPaginationResult) + scope.done() + } + ) + }) + + describe('backward pagination', (): void => { + test.each` + last | cursor + ${undefined} | ${uuid()} + ${5} | ${uuid()} + `( + 'returns outgoing payment list', + async ({ last, cursor }): Promise => { + const outgoingPaymentPaginationResult = + mockOutgoingPaymentPaginationResult({ + result: Array(last).fill(mockOutgoingPayment()) + }) + + const scope = nock(paymentPointer) + .get('/outgoing-payments') + .query({ ...(last ? { last } : {}), cursor }) + .reply(200, outgoingPaymentPaginationResult) + + const result = await listOutgoingPayments( + { + axiosInstance, + logger + }, + { + paymentPointer, + accessToken: 'accessToken' + }, + openApiValidators.successfulValidator, + { + last, + cursor + } + ) + expect(result).toStrictEqual(outgoingPaymentPaginationResult) + scope.done() + } + ) + }) + + test('throws if an outgoing payment does not pass validation', async (): Promise => { + const invalidOutgoingPayment = mockOutgoingPayment({ + sendAmount: { + assetCode: 'CAD', + assetScale: 2, + value: '5' + }, + sentAmount: { + assetCode: 'USD', + assetScale: 2, + value: '0' + } + }) + + const outgoingPaymentPaginationResult = + mockOutgoingPaymentPaginationResult({ + result: [invalidOutgoingPayment] + }) + + const scope = nock(paymentPointer) + .get('/outgoing-payments') + .reply(200, outgoingPaymentPaginationResult) + + await expect(() => + listOutgoingPayments( + { + axiosInstance, + logger + }, + { + paymentPointer, + accessToken: 'accessToken' + }, + openApiValidators.successfulValidator + ) + ).rejects.toThrowError(/Could not validate outgoing payment/) + scope.done() + }) + + test('throws if an outgoing payment does not pass open api validation', async (): Promise => { + const outgoingPaymentPaginationResult = + mockOutgoingPaymentPaginationResult() + + const scope = nock(paymentPointer) + .get('/outgoing-payments') + .reply(200, outgoingPaymentPaginationResult) + + await expect(() => + listOutgoingPayments( + { + axiosInstance, + logger + }, + { + paymentPointer, + accessToken: 'accessToken' + }, + openApiValidators.failedValidator + ) + ).rejects.toThrowError() + scope.done() + }) + }) + describe('createOutgoingPayment', (): void => { const quoteId = `${baseUrl}/quotes/${uuid()}` diff --git a/packages/open-payments/src/client/outgoing-payment.ts b/packages/open-payments/src/client/outgoing-payment.ts index 480e014a45..4c0d3cbc36 100644 --- a/packages/open-payments/src/client/outgoing-payment.ts +++ b/packages/open-payments/src/client/outgoing-payment.ts @@ -1,6 +1,12 @@ import { HttpMethod, ResponseValidator } from 'openapi' import { BaseDeps, RouteDeps } from '.' -import { CreateOutgoingPaymentArgs, getRSPath, OutgoingPayment } from '../types' +import { + CreateOutgoingPaymentArgs, + getRSPath, + OutgoingPayment, + OutgoingPaymentPaginationResult, + PaginationArgs +} from '../types' import { get, post } from './requests' interface GetArgs { @@ -8,6 +14,11 @@ interface GetArgs { accessToken: string } +interface ListGetArgs { + paymentPointer: string + accessToken: string +} + interface PostArgs { url: string body: T @@ -16,6 +27,10 @@ interface PostArgs { export interface OutgoingPaymentRoutes { get(args: GetArgs): Promise + list( + args: ListGetArgs, + pagination?: PaginationArgs + ): Promise create(args: PostArgs): Promise } @@ -30,6 +45,12 @@ export const createOutgoingPaymentRoutes = ( method: HttpMethod.GET }) + const listOutgoingPaymentOpenApiValidator = + openApi.createResponseValidator({ + path: getRSPath('/outgoing-payments'), + method: HttpMethod.GET + }) + const createOutgoingPaymentOpenApiValidator = openApi.createResponseValidator({ path: getRSPath('/outgoing-payments'), @@ -43,6 +64,13 @@ export const createOutgoingPaymentRoutes = ( args, getOutgoingPaymentOpenApiValidator ), + list: (getArgs: ListGetArgs, pagination?: PaginationArgs) => + listOutgoingPayments( + { axiosInstance, logger }, + getArgs, + listOutgoingPaymentOpenApiValidator, + pagination + ), create: (args: PostArgs) => createOutgoingPayment( { axiosInstance, logger }, @@ -100,6 +128,47 @@ export const createOutgoingPayment = async ( } } +export const listOutgoingPayments = async ( + deps: BaseDeps, + getArgs: ListGetArgs, + validateOpenApiResponse: ResponseValidator, + pagination?: PaginationArgs +) => { + const { axiosInstance, logger } = deps + const { accessToken, paymentPointer } = getArgs + const url = `${paymentPointer}${getRSPath('/outgoing-payments')}` + + const outgoingPayments = await get( + { axiosInstance, logger }, + { + url, + accessToken, + ...(pagination ? { queryParams: { ...pagination } } : {}) + }, + validateOpenApiResponse + ) + + for (const outgoingPayment of outgoingPayments.result) { + try { + validateOutgoingPayment(outgoingPayment) + } catch (error) { + const errorMessage = 'Could not validate outgoing payment' + logger.error( + { + url, + validateError: error?.message, + outgoingPaymentId: outgoingPayment.id + }, + errorMessage + ) + + throw new Error(errorMessage) + } + } + + return outgoingPayments +} + export const validateOutgoingPayment = ( payment: OutgoingPayment ): OutgoingPayment => { diff --git a/packages/open-payments/src/client/requests.test.ts b/packages/open-payments/src/client/requests.test.ts index cc84f27d6c..2c64d8bdaa 100644 --- a/packages/open-payments/src/client/requests.test.ts +++ b/packages/open-payments/src/client/requests.test.ts @@ -114,6 +114,46 @@ describe('requests', (): void => { ) }) + test.each` + title | queryParams + ${'all defined values'} | ${{ first: 5, cursor: 'id' }} + ${'some undefined values'} | ${{ first: 5, cursor: undefined }} + ${'all undefined values'} | ${{ first: undefined, cursor: undefined }} + `( + 'properly sets query params with $title', + async ({ queryParams }): Promise => { + const cleanedQueryParams = Object.fromEntries( + Object.entries(queryParams).filter(([_, v]) => v != null) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) as any + + const scope = nock(baseUrl) + .matchHeader('Signature', (sig) => sig === undefined) + .matchHeader('Signature-Input', (sigInput) => sigInput === undefined) + .get('/incoming-payments') + .query(cleanedQueryParams) + .reply(200) + + await get( + { axiosInstance, logger }, + { + url: `${baseUrl}/incoming-payments`, + queryParams + }, + responseValidators.successfulValidator + ) + scope.done() + + expect(axiosInstance.get).toHaveBeenCalledWith( + `${baseUrl}/incoming-payments`, + { + headers: {}, + params: cleanedQueryParams + } + ) + } + ) + test('calls validator function properly', async (): Promise => { const status = 200 const body = { diff --git a/packages/open-payments/src/client/requests.ts b/packages/open-payments/src/client/requests.ts index 2c3f9041a0..5d6afed962 100644 --- a/packages/open-payments/src/client/requests.ts +++ b/packages/open-payments/src/client/requests.ts @@ -7,6 +7,7 @@ import { createSignatureHeaders } from './signatures' interface GetArgs { url: string + queryParams?: Record accessToken?: string } @@ -16,6 +17,9 @@ interface PostArgs { accessToken?: string } +const removeEmptyValues = (obj: Record) => + Object.fromEntries(Object.entries(obj).filter(([_, v]) => v != null)) + export const get = async ( deps: BaseDeps, args: GetArgs, @@ -37,7 +41,8 @@ export const get = async ( ? { Authorization: `GNAP ${accessToken}` } - : {} + : {}, + params: args.queryParams ? removeEmptyValues(args.queryParams) : undefined }) try { diff --git a/packages/open-payments/src/test/helpers.ts b/packages/open-payments/src/test/helpers.ts index 8ee3f0808a..6cd16da142 100644 --- a/packages/open-payments/src/test/helpers.ts +++ b/packages/open-payments/src/test/helpers.ts @@ -8,7 +8,8 @@ import { GrantRequest, GrantContinuationRequest, NonInteractiveGrant, - OutgoingPayment + OutgoingPayment, + OutgoingPaymentPaginationResult } from '../types' import base64url from 'base64url' import { v4 as uuid } from 'uuid' @@ -99,6 +100,26 @@ export const mockOutgoingPayment = ( ...overrides }) +export const mockOutgoingPaymentPaginationResult = ( + overrides?: Partial +): OutgoingPaymentPaginationResult => { + const result = overrides?.result || [ + mockOutgoingPayment(), + mockOutgoingPayment(), + mockOutgoingPayment() + ] + + return { + result, + pagination: overrides?.pagination || { + startCursor: result[0].id, + hasNextPage: true, + hasPreviousPage: true, + endCursor: result[result.length - 1].id + } + } +} + export const mockInteractiveGrant = ( overrides?: Partial ): InteractiveGrant => ({ diff --git a/packages/open-payments/src/types.ts b/packages/open-payments/src/types.ts index 9debd897bb..691ed0106c 100644 --- a/packages/open-payments/src/types.ts +++ b/packages/open-payments/src/types.ts @@ -20,6 +20,20 @@ export type ILPStreamConnection = export type OutgoingPayment = RSComponents['schemas']['outgoing-payment'] export type CreateOutgoingPaymentArgs = RSOperations['create-outgoing-payment']['requestBody']['content']['application/json'] +type PaginationResult = { + pagination: RSComponents['schemas']['page-info'] + result: T[] +} +export type OutgoingPaymentPaginationResult = PaginationResult +export type ForwardPagination = + RSComponents['schemas']['forward-pagination'] & { + last?: never + } +export type BackwardPagination = + RSComponents['schemas']['backward-pagination'] & { + first?: never + } +export type PaginationArgs = ForwardPagination | BackwardPagination export type PaymentPointer = RSComponents['schemas']['payment-pointer'] export type JWK = RSComponents['schemas']['json-web-key'] export type JWKS = RSComponents['schemas']['json-web-key-set'] From ac84eb1abe267bb742c32881c650e31f0a297a25 Mon Sep 17 00:00:00 2001 From: Nathan Lie Date: Wed, 14 Dec 2022 09:34:15 -0800 Subject: [PATCH 03/11] feat(auth): add typings to request contexts & bodies (#821) --- packages/auth/src/accessToken/routes.ts | 41 ++++++-- packages/auth/src/app.ts | 42 +++++--- packages/auth/src/grant/routes.ts | 113 ++++++++++++++++----- packages/auth/src/openapi/id-provider.yaml | 12 +++ packages/openapi/src/middleware.ts | 3 +- 5 files changed, 164 insertions(+), 47 deletions(-) diff --git a/packages/auth/src/accessToken/routes.ts b/packages/auth/src/accessToken/routes.ts index d2793f4a01..f7a3c102eb 100644 --- a/packages/auth/src/accessToken/routes.ts +++ b/packages/auth/src/accessToken/routes.ts @@ -6,6 +6,29 @@ import { AccessTokenService, Introspection } from './service' import { accessToBody } from '../shared/utils' import { ClientService } from '../client/service' +type TokenRequest = Omit & { + body?: BodyT +} + +type TokenContext = Omit & { + request: TokenRequest +} + +type ManagementRequest = Omit & { + params?: Record<'id', string> +} + +type ManagementContext = Omit & { + request: ManagementRequest +} + +interface IntrospectBody { + access_token: string +} +export type IntrospectContext = TokenContext +export type RevokeContext = ManagementContext +export type RotateContext = ManagementContext + interface ServiceDependencies { config: IAppConfig logger: Logger @@ -14,9 +37,9 @@ interface ServiceDependencies { } export interface AccessTokenRoutes { - introspect(ctx: AppContext): Promise - revoke(ctx: AppContext): Promise - rotate(ctx: AppContext): Promise + introspect(ctx: IntrospectContext): Promise + revoke(ctx: RevokeContext): Promise + rotate(ctx: RotateContext): Promise } export function createAccessTokenRoutes( @@ -27,15 +50,15 @@ export function createAccessTokenRoutes( }) const deps = { ...deps_, logger } return { - introspect: (ctx: AppContext) => introspectToken(deps, ctx), - revoke: (ctx: AppContext) => revokeToken(deps, ctx), - rotate: (ctx: AppContext) => rotateToken(deps, ctx) + introspect: (ctx: IntrospectContext) => introspectToken(deps, ctx), + revoke: (ctx: RevokeContext) => revokeToken(deps, ctx), + rotate: (ctx: RotateContext) => rotateToken(deps, ctx) } } async function introspectToken( deps: ServiceDependencies, - ctx: AppContext + ctx: IntrospectContext ): Promise { const { body } = ctx.request const introspectionResult = await deps.accessTokenService.introspect( @@ -68,7 +91,7 @@ function introspectionToBody(result: Introspection) { async function revokeToken( deps: ServiceDependencies, - ctx: AppContext + ctx: RevokeContext ): Promise { const { id: managementId } = ctx.params await deps.accessTokenService.revoke(managementId) @@ -77,7 +100,7 @@ async function revokeToken( async function rotateToken( deps: ServiceDependencies, - ctx: AppContext + ctx: RotateContext ): Promise { // TODO: verify Authorization: GNAP ${accessToken} contains correct token value const { id: managementId } = ctx.params diff --git a/packages/auth/src/app.ts b/packages/auth/src/app.ts index 85615112b1..a2005ef7a6 100644 --- a/packages/auth/src/app.ts +++ b/packages/auth/src/app.ts @@ -13,7 +13,20 @@ import Router from '@koa/router' import { IAppConfig } from './config/app' import { ClientService } from './client/service' import { GrantService } from './grant/service' -import { AccessTokenRoutes } from './accessToken/routes' +import { + CreateContext, + ContinueContext, + StartContext, + GetContext, + ChooseContext, + FinishContext +} from './grant/routes' +import { + AccessTokenRoutes, + IntrospectContext, + RevokeContext, + RotateContext +} from './accessToken/routes' import { createValidatorMiddleware, HttpMethod } from 'openapi' import { @@ -195,7 +208,7 @@ export class App { // Grant Initiation this.publicRouter.post( '/', - createValidatorMiddleware(openApi.authServerSpec, { + createValidatorMiddleware(openApi.authServerSpec, { path: '/', method: HttpMethod.POST }), @@ -208,7 +221,7 @@ export class App { // Grant Continue this.publicRouter.post( '/continue/:id', - createValidatorMiddleware(openApi.authServerSpec, { + createValidatorMiddleware(openApi.authServerSpec, { path: '/continue/{id}', method: HttpMethod.POST }), @@ -221,7 +234,7 @@ export class App { // Token Rotation this.publicRouter.post( '/token/:id', - createValidatorMiddleware(openApi.authServerSpec, { + createValidatorMiddleware(openApi.authServerSpec, { path: '/token/{id}', method: HttpMethod.POST }), @@ -234,7 +247,7 @@ export class App { // Token Revocation this.publicRouter.delete( '/token/:id', - createValidatorMiddleware(openApi.authServerSpec, { + createValidatorMiddleware(openApi.authServerSpec, { path: '/token/{id}', method: HttpMethod.DELETE }), @@ -248,10 +261,13 @@ export class App { // Token Introspection this.publicRouter.post( '/introspect', - createValidatorMiddleware(openApi.tokenIntrospectionSpec, { - path: '/introspect', - method: HttpMethod.POST - }), + createValidatorMiddleware( + openApi.tokenIntrospectionSpec, + { + path: '/introspect', + method: HttpMethod.POST + } + ), accessTokenRoutes.introspect ) @@ -261,7 +277,7 @@ export class App { // Interaction start this.publicRouter.get( '/interact/:id/:nonce', - createValidatorMiddleware(openApi.idpSpec, { + createValidatorMiddleware(openApi.idpSpec, { path: '/interact/{id}/{nonce}', method: HttpMethod.GET }), @@ -271,7 +287,7 @@ export class App { // Interaction finish this.publicRouter.get( '/interact/:id/:nonce/finish', - createValidatorMiddleware(openApi.idpSpec, { + createValidatorMiddleware(openApi.idpSpec, { path: '/interact/{id}/{nonce}/finish', method: HttpMethod.GET }), @@ -281,7 +297,7 @@ export class App { // Grant lookup this.publicRouter.get( '/grant/:id/:nonce', - createValidatorMiddleware(openApi.idpSpec, { + createValidatorMiddleware(openApi.idpSpec, { path: '/grant/{id}/{nonce}', method: HttpMethod.GET }), @@ -291,7 +307,7 @@ export class App { // Grant accept/reject this.publicRouter.post( '/grant/:id/:nonce/:choice', - createValidatorMiddleware(openApi.idpSpec, { + createValidatorMiddleware(openApi.idpSpec, { path: '/grant/{id}/{nonce}/{choice}', method: HttpMethod.POST }), diff --git a/packages/auth/src/grant/routes.ts b/packages/auth/src/grant/routes.ts index a44c9a6bb1..1dca0b7834 100644 --- a/packages/auth/src/grant/routes.ts +++ b/packages/auth/src/grant/routes.ts @@ -1,7 +1,9 @@ import * as crypto from 'crypto' import { URL } from 'url' +import { ParsedUrlQuery } from 'querystring' + import { AppContext } from '../app' -import { GrantService } from './service' +import { GrantService, GrantRequest as GrantRequestBody } from './service' import { Grant, GrantState } from './model' import { Access } from '../access/model' import { ClientService } from '../client/service' @@ -24,16 +26,84 @@ interface ServiceDependencies extends BaseService { config: IAppConfig } +type GrantRequest = Omit< + AppContext['request'], + 'body' +> & { + body: BodyT + query: ParsedUrlQuery & QueryT +} + +type GrantContext = Omit< + AppContext, + 'request' +> & { + request: GrantRequest + clientKeyId: string +} + +export type CreateContext = GrantContext + +interface GrantContinueBody { + interact_ref: string +} + +interface GrantContinueParams { + id: string +} +export type ContinueContext = GrantContext< + GrantContinueBody, + GrantContinueParams +> + +type InteractionRequest< + BodyT = never, + QueryT = ParsedUrlQuery, + ParamsT = { [key: string]: string } +> = Omit & { + body: BodyT + query: ParsedUrlQuery & QueryT + params: ParamsT +} + +type InteractionContext = Omit & { + request: InteractionRequest +} + +interface StartQuery { + clientName: string + clientUri: string +} + +interface InteractionParams { + id: string + nonce: string +} +export type StartContext = InteractionContext + +export type GetContext = InteractionContext + +export enum GrantChoices { + Accept = 'accept', + Reject = 'reject' +} +interface ChooseParams extends InteractionParams { + choice: string +} +export type ChooseContext = InteractionContext + +export type FinishContext = InteractionContext + export interface GrantRoutes { - create(ctx: AppContext): Promise + create(ctx: CreateContext): Promise // TODO: factor this out into separate routes service interaction: { - start(ctx: AppContext): Promise - finish(ctx: AppContext): Promise - acceptOrReject(ctx: AppContext): Promise - details(ctx: AppContext): Promise + start(ctx: StartContext): Promise + finish(ctx: FinishContext): Promise + acceptOrReject(ctx: ChooseContext): Promise + details(ctx: GetContext): Promise } - continue(ctx: AppContext): Promise + continue(ctx: ContinueContext): Promise } export function createGrantRoutes({ @@ -57,20 +127,20 @@ export function createGrantRoutes({ config } return { - create: (ctx: AppContext) => createGrantInitiation(deps, ctx), + create: (ctx: CreateContext) => createGrantInitiation(deps, ctx), interaction: { - start: (ctx: AppContext) => startInteraction(deps, ctx), - finish: (ctx: AppContext) => finishInteraction(deps, ctx), - acceptOrReject: (ctx: AppContext) => handleGrantChoice(deps, ctx), - details: (ctx: AppContext) => getGrantDetails(deps, ctx) + start: (ctx: StartContext) => startInteraction(deps, ctx), + finish: (ctx: FinishContext) => finishInteraction(deps, ctx), + acceptOrReject: (ctx: ChooseContext) => handleGrantChoice(deps, ctx), + details: (ctx: GetContext) => getGrantDetails(deps, ctx) }, - continue: (ctx: AppContext) => continueGrant(deps, ctx) + continue: (ctx: ContinueContext) => continueGrant(deps, ctx) } } async function createGrantInitiation( deps: ServiceDependencies, - ctx: AppContext + ctx: CreateContext ): Promise { if ( !ctx.accepts('application/json') || @@ -173,7 +243,7 @@ async function createGrantInitiation( async function getGrantDetails( deps: ServiceDependencies, - ctx: AppContext + ctx: GetContext ): Promise { const secret = ctx.headers?.['x-idp-secret'] const { config, grantService } = deps @@ -205,7 +275,7 @@ async function getGrantDetails( async function startInteraction( deps: ServiceDependencies, - ctx: AppContext + ctx: StartContext ): Promise { deps.logger.info( { @@ -240,15 +310,10 @@ async function startInteraction( ctx.redirect(interactionUrl.toString()) } -export enum GrantChoices { - Accept = 'accept', - Reject = 'reject' -} - // TODO: allow idp to specify the reason for rejection async function handleGrantChoice( deps: ServiceDependencies, - ctx: AppContext + ctx: ChooseContext ): Promise { // TODO: check redis for a session const { id: interactId, nonce, choice } = ctx.params @@ -311,7 +376,7 @@ async function handleGrantChoice( async function finishInteraction( deps: ServiceDependencies, - ctx: AppContext + ctx: FinishContext ): Promise { const { id: interactId, nonce } = ctx.params const sessionNonce = ctx.session.nonce @@ -361,7 +426,7 @@ async function finishInteraction( async function continueGrant( deps: ServiceDependencies, - ctx: AppContext + ctx: ContinueContext ): Promise { const { id: continueId } = ctx.params const continueToken = (ctx.headers['authorization'] as string)?.split( diff --git a/packages/auth/src/openapi/id-provider.yaml b/packages/auth/src/openapi/id-provider.yaml index 1d9529f4ac..e105ae76ac 100644 --- a/packages/auth/src/openapi/id-provider.yaml +++ b/packages/auth/src/openapi/id-provider.yaml @@ -51,6 +51,18 @@ paths: name: nonce required: true description: 'Unique value to be used in the calculation of the "hash" query parameter sent to the callback URI, must be sufficiently random to be unguessable by an attacker. MUST be generated by the client instance as a unique value for this request.' + - schema: + type: string + name: clientName + in: path + required: true + description: 'Name of the client that created the grant' + - schema: + type: string + name: clientUri + in: path + required: true + description: 'URI of the client that created the grant' description: 'To start the user interaction for grant approval, this endpoint redirects the user to an Identity provider endpoint for authentication.' tags: - front-channel diff --git a/packages/openapi/src/middleware.ts b/packages/openapi/src/middleware.ts index c90572b24d..24a755a317 100644 --- a/packages/openapi/src/middleware.ts +++ b/packages/openapi/src/middleware.ts @@ -2,7 +2,7 @@ import { OpenAPI, RequestOptions, isValidationError } from './' import Koa from 'koa' -export function createValidatorMiddleware( +export function createValidatorMiddleware( spec: OpenAPI, options: RequestOptions ): (ctx: Koa.Context, next: () => Promise) => Promise { @@ -31,6 +31,7 @@ export function createValidatorMiddleware( } else if (isValidationError(err)) { ctx.throw(err.status ?? 500, err.errors[0]) } else { + console.log('err=', err) ctx.throw(500) } } From 73e250df90dfa7a7fb239c3ce5dbb057fe50572a Mon Sep 17 00:00:00 2001 From: Sabine Schaller Date: Thu, 15 Dec 2022 09:35:30 +0100 Subject: [PATCH 04/11] feat: move http signature related code to http-signature-utils package (#797) * feat(http-signature-utils): initial commit * chore(http-signature-utils): update lockfile * fix(http-signature-utils): fix package.json * fix: add http-signature-utils to tsconfig * fix: include http-signature-utils in gh workflow * fix: gh workflow build deps * fix: builds * feat(HSU): pass keyId * feat(HSU): add Dockerfile and add app to local infrastructure * feat(HSU): move content digest creation into HSU * Revert "feat(HSU): move content digest creation into HSU" This reverts commit f6eae9c2cd37cf4ba0c8ccdb9d97f3e361b248e2. * feat(MAP): load private key * chore: fix build:deps * fix(MAP): graphql url * feat(HSU): add content headers to app * fix(HSU): remove `conent-type` and `content-length` headers from app * feat(HSU): add content digest header to app * fix(HSU): allow for lower case headers * feat(HSU): move sig verification from auth to HSU * fix: export and imports * feat(HSU): move sig verification to HSU * refactor(HSU): remove koa context from HSU * fix(backend): middleware context * fix(backend): imports * fix(HSU): remove conent type and length headers from app response * fix(HSU): add missing dependency * fix: request URL * fix(local): readd redis network * fix(HSU): removing whitespace * feat(local): enable sig validation * test(HSU): add tests * fix: types * refactor(auth): remove console.log * fix(MAP): quote fees * docs(HSU): add Readme * feat(HSU): add postman scripts * fix(HSU): formatting * fix(HSU): add postman scripts to lintignore file * refactor(HSU): verification * fix(HSU): key tmp dir * refactor(infrastructure): rename key files * chore(auth): remove jose * refactor(open-payments): use createHeaders from utils package * style(HSU): rename validateHttpSigHeaders and verifySigAndChallenge * refactor(auth): only store keyId, not entire key * refactor: remove JWKWithRequired * refactor(HSU): remove unnecessary type cast * chore: update gh workflow actions version * chore(HSU): move postman scripts out of src * test(HSU): add positive header validation tests * style(HSU): rename verification -> validation --- .eslintignore | 3 +- .github/workflows/lint_test_build.yml | 12 +- infrastructure/local/docker-compose.yml | 19 +- infrastructure/local/peer-docker-compose.yml | 17 +- infrastructure/local/peer-private-key.pem | 3 + infrastructure/local/private-key.pem | 3 + packages/auth/package.json | 3 +- packages/auth/src/accessToken/routes.test.ts | 34 ++-- packages/auth/src/accessToken/service.test.ts | 29 ++- packages/auth/src/accessToken/service.ts | 5 +- packages/auth/src/client/service.test.ts | 7 +- packages/auth/src/client/service.ts | 17 +- packages/auth/src/grant/routes.test.ts | 13 +- packages/auth/src/grant/service.test.ts | 18 +- packages/auth/src/index.ts | 2 - .../auth/src/signature/middleware.test.ts | 163 +++------------- packages/auth/src/signature/middleware.ts | 176 +++--------------- packages/auth/src/tests/context.ts | 34 ++-- packages/auth/src/tests/signature.ts | 99 ---------- packages/backend/package.json | 4 +- packages/backend/src/config/app.ts | 33 +--- .../src/open_payments/auth/middleware.test.ts | 95 +++++----- .../src/open_payments/auth/middleware.ts | 17 +- .../backend/src/open_payments/auth/service.ts | 7 +- .../backend/src/paymentPointerKey/model.ts | 4 +- .../src/paymentPointerKey/routes.test.ts | 3 +- .../backend/src/paymentPointerKey/routes.ts | 2 +- .../backend/src/paymentPointerKey/service.ts | 4 +- packages/http-signature-utils/Dockerfile | 20 ++ packages/http-signature-utils/README.md | 139 ++++++++++++++ packages/http-signature-utils/jest.config.js | 17 ++ packages/http-signature-utils/package.json | 34 ++++ .../postman-scripts/preRequestSignatures.js | 45 +++++ .../preRequestSignaturesGrantRequest.js | 69 +++++++ packages/http-signature-utils/src/app.ts | 40 ++++ packages/http-signature-utils/src/index.ts | 7 + .../src/test-utils/keys.ts | 14 ++ .../http-signature-utils/src/utils/headers.ts | 50 +++++ .../src/utils}/jwk.test.ts | 0 .../src/utils}/jwk.ts | 14 +- .../src/utils/key.test.ts} | 2 +- .../http-signature-utils/src/utils/key.ts | 32 ++++ .../src/utils}/signatures.ts | 6 +- .../src/utils/validation.test.ts | 91 +++++++++ .../src/utils/validation.ts | 116 ++++++++++++ packages/http-signature-utils/tsconfig.json | 11 ++ .../app/lib/apolloClient.ts | 2 +- .../app/lib/crypto.server.ts | 22 --- .../app/lib/parse_config.ts | 20 +- .../mock-account-provider/app/lib/run_seed.ts | 25 +-- .../app/routes/quotes.ts | 2 +- packages/mock-account-provider/package.json | 4 +- packages/open-payments/package.json | 4 +- packages/open-payments/src/client/requests.ts | 27 ++- packages/open-payments/src/index.ts | 2 - pnpm-lock.yaml | 169 ++++++++++++++--- tsconfig.json | 3 + 57 files changed, 1164 insertions(+), 649 deletions(-) create mode 100644 infrastructure/local/peer-private-key.pem create mode 100644 infrastructure/local/private-key.pem create mode 100644 packages/http-signature-utils/Dockerfile create mode 100644 packages/http-signature-utils/README.md create mode 100644 packages/http-signature-utils/jest.config.js create mode 100644 packages/http-signature-utils/package.json create mode 100644 packages/http-signature-utils/postman-scripts/preRequestSignatures.js create mode 100644 packages/http-signature-utils/postman-scripts/preRequestSignaturesGrantRequest.js create mode 100644 packages/http-signature-utils/src/app.ts create mode 100644 packages/http-signature-utils/src/index.ts create mode 100644 packages/http-signature-utils/src/test-utils/keys.ts create mode 100644 packages/http-signature-utils/src/utils/headers.ts rename packages/{open-payments/src => http-signature-utils/src/utils}/jwk.test.ts (100%) rename packages/{open-payments/src => http-signature-utils/src/utils}/jwk.ts (76%) rename packages/{backend/src/config/app.test.ts => http-signature-utils/src/utils/key.test.ts} (98%) create mode 100644 packages/http-signature-utils/src/utils/key.ts rename packages/{open-payments/src/client => http-signature-utils/src/utils}/signatures.ts (88%) create mode 100644 packages/http-signature-utils/src/utils/validation.test.ts create mode 100644 packages/http-signature-utils/src/utils/validation.ts create mode 100644 packages/http-signature-utils/tsconfig.json delete mode 100644 packages/mock-account-provider/app/lib/crypto.server.ts diff --git a/.eslintignore b/.eslintignore index 2b51267516..b6bd735f63 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,5 @@ public generated dist -build \ No newline at end of file +build +postman-scripts \ No newline at end of file diff --git a/.github/workflows/lint_test_build.yml b/.github/workflows/lint_test_build.yml index f13566eb1e..70d820fc98 100644 --- a/.github/workflows/lint_test_build.yml +++ b/.github/workflows/lint_test_build.yml @@ -80,9 +80,18 @@ jobs: steps: - uses: actions/checkout@v3 - uses: ./.github/workflows/rafiki/env-setup - - run: pnpm --filter openapi build + - run: pnpm --filter open-payments build:deps - run: pnpm --filter open-payments test + http-signature-utils: + runs-on: ubuntu-latest + needs: checkout + timeout-minutes: 5 + steps: + - uses: actions/checkout@v3 + - uses: ./.github/workflows/rafiki/env-setup + - run: pnpm --filter http-signature-utils test + build: runs-on: ubuntu-latest timeout-minutes: 5 @@ -93,6 +102,7 @@ jobs: - openapi - mock-account-provider - open-payments + - http-signature-utils steps: - uses: actions/checkout@v3 - uses: ./.github/workflows/rafiki/env-setup diff --git a/infrastructure/local/docker-compose.yml b/infrastructure/local/docker-compose.yml index f0302abf36..dc820382c2 100644 --- a/infrastructure/local/docker-compose.yml +++ b/infrastructure/local/docker-compose.yml @@ -14,7 +14,7 @@ services: NODE_ENV: development AUTH_DATABASE_URL: postgresql://auth:auth@database/auth INTROSPECTION_HTTPSIG: "false" - BYPASS_SIGNATURE_VALIDATION: "true" + BYPASS_SIGNATURE_VALIDATION: "false" depends_on: - tigerbeetle - database @@ -32,8 +32,10 @@ services: LOG_LEVEL: debug PORT: 80 SEED_FILE_LOCATION: /workspace/seed.primary.yml + KEY_FILE: /workspace/private-key.pem volumes: - ./seed.primary.yml:/workspace/seed.primary.yml + - ./private-key.pem:/workspace/private-key.pem depends_on: - backend backend: @@ -70,7 +72,7 @@ services: PRICES_URL: http://fynbos/prices REDIS_URL: redis://redis:6379/0 QUOTE_URL: http://fynbos/quotes - BYPASS_SIGNATURE_VALIDATION: "true" + BYPASS_SIGNATURE_VALIDATION: "false" PAYMENT_POINTER_URL: https://backend/.well-known/pay depends_on: - tigerbeetle @@ -121,6 +123,19 @@ services: restart: unless-stopped networks: - rafiki + signatures: + build: + context: ../.. + dockerfile: ./packages/http-signature-utils/Dockerfile + restart: always + ports: + - '3040:3000' + environment: + KEY_FILE: /workspace/private-key.pem + volumes: + - ./private-key.pem:/workspace/private-key.pem + networks: + - rafiki volumes: database-data: # named volumes can be managed easier using docker-compose tigerbeetle-data: # named volumes can be managed easier using docker-compose diff --git a/infrastructure/local/peer-docker-compose.yml b/infrastructure/local/peer-docker-compose.yml index 5e41b41714..902cceec2d 100644 --- a/infrastructure/local/peer-docker-compose.yml +++ b/infrastructure/local/peer-docker-compose.yml @@ -14,7 +14,7 @@ services: NODE_ENV: development AUTH_DATABASE_URL: postgresql://peerauth:peerauth@database/peerauth INTROSPECTION_HTTPSIG: "false" - BYPASS_SIGNATURE_VALIDATION: "true" + BYPASS_SIGNATURE_VALIDATION: "false" AUTH_SERVER_DOMAIN: "http://localhost:4006" peer-backend: image: ghcr.io/interledger/rafiki-backend:latest @@ -50,7 +50,7 @@ services: PRICES_URL: http://local-bank/prices REDIS_URL: redis://redis:6379/1 QUOTE_URL: http://local-bank/quote - BYPASS_SIGNATURE_VALIDATION: "true" + BYPASS_SIGNATURE_VALIDATION: "false" PAYMENT_POINTER_URL: https://peer-backend/.well-known/pay local-bank: build: @@ -66,10 +66,23 @@ services: LOG_LEVEL: debug PORT: 80 SEED_FILE_LOCATION: /workspace/seed.peer.yml + KEY_FILE: /workspace/private-key.pem volumes: - ./seed.peer.yml:/workspace/seed.peer.yml + - ./peer-private-key.pem:/workspace/private-key.pem depends_on: - peer-backend + peer-signatures: + build: + context: ../.. + dockerfile: ./packages/http-signature-utils/Dockerfile + restart: always + ports: + - '3041:3000' + environment: + KEY_FILE: /workspace/private-key.pem + volumes: + - ./peer-private-key.pem:/workspace/private-key.pem networks: local_rafiki: external: true diff --git a/infrastructure/local/peer-private-key.pem b/infrastructure/local/peer-private-key.pem new file mode 100644 index 0000000000..7fa97c747d --- /dev/null +++ b/infrastructure/local/peer-private-key.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIEqezmcPhOE8bkwN+jQrppfRYzGIdFTVWQGTHJIKpz88 +-----END PRIVATE KEY----- diff --git a/infrastructure/local/private-key.pem b/infrastructure/local/private-key.pem new file mode 100644 index 0000000000..5814233c8c --- /dev/null +++ b/infrastructure/local/private-key.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEICxfM9mUurUGnwlMMQEDclDEQnX7c49BoGKOB48URBxO +-----END PRIVATE KEY----- diff --git a/packages/auth/package.json b/packages/auth/package.json index 71c470097a..152d6e11f6 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -21,8 +21,8 @@ "@koa/router": "^12.0.0", "ajv": "^8.11.2", "axios": "^0.27.2", + "http-signature-utils": "workspace:../http-signature-utils", "httpbis-digest-headers": "github:interledger/httpbis-digest-headers", - "jose": "^4.9.0", "knex": "^0.95", "koa": "^2.13.4", "koa-bodyparser": "^4.3.0", @@ -39,6 +39,7 @@ "uuid": "^8.3.2" }, "devDependencies": { + "@faker-js/faker": "^7.4.0", "@types/jest": "^28.1.8", "@types/koa": "2.13.5", "@types/koa-bodyparser": "^4.3.7", diff --git a/packages/auth/src/accessToken/routes.test.ts b/packages/auth/src/accessToken/routes.test.ts index 7cfe504ffb..c2b2b7d185 100644 --- a/packages/auth/src/accessToken/routes.test.ts +++ b/packages/auth/src/accessToken/routes.test.ts @@ -17,8 +17,12 @@ import { AccessToken } from './model' import { Access } from '../access/model' import { AccessTokenRoutes } from './routes' import { createContext } from '../tests/context' -import { generateTestKeys } from '../tests/signature' -import { JWKWithRequired } from '../client/service' +import { + generateJwk, + generateTestKeys, + JWK, + TestKeys +} from 'http-signature-utils' describe('Access Token Routes', (): void => { let deps: IocContract @@ -26,7 +30,8 @@ describe('Access Token Routes', (): void => { let knex: Knex let trx: Knex.Transaction let accessTokenRoutes: AccessTokenRoutes - let testJwk: JWKWithRequired + let testKeys: TestKeys + let testClientKey: JWK beforeAll(async (): Promise => { deps = await initIocContainer(Config) @@ -36,8 +41,11 @@ describe('Access Token Routes', (): void => { const openApi = await deps.use('openApi') jestOpenAPI(openApi.authServerSpec) - const keys = await generateTestKeys() - testJwk = keys.publicKey + testKeys = await generateTestKeys() + testClientKey = generateJwk({ + privateKey: testKeys.privateKey, + keyId: testKeys.keyId + }) }) afterEach(async (): Promise => { @@ -98,7 +106,7 @@ describe('Access Token Routes', (): void => { beforeEach(async (): Promise => { grant = await Grant.query(trx).insertAndFetch({ ...BASE_GRANT, - clientKeyId: testJwk.kid + clientKeyId: testKeys.keyId }) access = await Access.query(trx).insertAndFetch({ grantId: grant.id, @@ -141,7 +149,7 @@ describe('Access Token Routes', (): void => { const scope = nock(CLIENT) .get('/jwks.json') .reply(200, { - keys: [testJwk] + keys: [testClientKey] }) const ctx = createContext( @@ -179,7 +187,7 @@ describe('Access Token Routes', (): void => { ], key: { proof: 'httpsig', - jwk: testJwk + jwk: testClientKey }, client_id: clientId }) @@ -190,7 +198,7 @@ describe('Access Token Routes', (): void => { const scope = nock(CLIENT) .get('/jwks.json') .reply(200, { - keys: [testJwk] + keys: [testClientKey] }) const tokenCreatedDate = new Date(token.createdAt) const now = new Date( @@ -238,7 +246,7 @@ describe('Access Token Routes', (): void => { beforeEach(async (): Promise => { grant = await Grant.query(trx).insertAndFetch({ ...BASE_GRANT, - clientKeyId: testJwk.kid + clientKeyId: testKeys.keyId }) token = await AccessToken.query(trx).insertAndFetch({ grantId: grant.id, @@ -269,7 +277,7 @@ describe('Access Token Routes', (): void => { const scope = nock(CLIENT) .get('/jwks.json') .reply(200, { - keys: [testJwk] + keys: [testClientKey] }) const ctx = createContext( @@ -298,7 +306,7 @@ describe('Access Token Routes', (): void => { const scope = nock(CLIENT) .get('/jwks.json') .reply(200, { - keys: [testJwk] + keys: [testClientKey] }) const ctx = createContext( @@ -333,7 +341,7 @@ describe('Access Token Routes', (): void => { beforeEach(async (): Promise => { grant = await Grant.query(trx).insertAndFetch({ ...BASE_GRANT, - clientKeyId: testJwk.kid + clientKeyId: testKeys.keyId }) access = await Access.query(trx).insertAndFetch({ grantId: grant.id, diff --git a/packages/auth/src/accessToken/service.test.ts b/packages/auth/src/accessToken/service.test.ts index 232c48a985..23f7d27493 100644 --- a/packages/auth/src/accessToken/service.test.ts +++ b/packages/auth/src/accessToken/service.test.ts @@ -8,7 +8,7 @@ import { v4 } from 'uuid' import { createTestApp, TestContainer } from '../tests/app' import { Config } from '../config/app' import { IocContract } from '@adonisjs/fold' -import { initIocContainer, JWKWithRequired } from '..' +import { initIocContainer } from '..' import { AppServices } from '../app' import { truncateTables } from '../tests/tableManager' import { FinishMethod, Grant, GrantState, StartMethod } from '../grant/model' @@ -16,7 +16,12 @@ import { AccessType, Action } from '../access/types' import { AccessToken } from './model' import { AccessTokenService } from './service' import { Access } from '../access/model' -import { generateTestKeys } from '../tests/signature' +import { + generateJwk, + generateTestKeys, + JWK, + TestKeys +} from 'http-signature-utils' describe('Access Token Service', (): void => { let deps: IocContract @@ -24,7 +29,8 @@ describe('Access Token Service', (): void => { let knex: Knex let trx: Knex.Transaction let accessTokenService: AccessTokenService - let testJwk: JWKWithRequired + let testKeys: TestKeys + let testClientKey: JWK beforeAll(async (): Promise => { deps = await initIocContainer(Config) @@ -32,8 +38,11 @@ describe('Access Token Service', (): void => { knex = await deps.use('knex') accessTokenService = await deps.use('accessTokenService') - const keys = await generateTestKeys() - testJwk = keys.publicKey + testKeys = await generateTestKeys() + testClientKey = generateJwk({ + privateKey: testKeys.privateKey, + keyId: testKeys.keyId + }) }) afterEach(async (): Promise => { @@ -81,7 +90,7 @@ describe('Access Token Service', (): void => { beforeEach(async (): Promise => { grant = await Grant.query(trx).insertAndFetch({ ...BASE_GRANT, - clientKeyId: testJwk.kid, + clientKeyId: testKeys.keyId, continueToken: crypto.randomBytes(8).toString('hex').toUpperCase(), continueId: v4(), interactId: v4(), @@ -146,7 +155,7 @@ describe('Access Token Service', (): void => { const scope = nock(CLIENT) .get('/jwks.json') .reply(200, { - keys: [testJwk] + keys: [testClientKey] }) const introspection = await accessTokenService.introspect(token.value) @@ -155,7 +164,7 @@ describe('Access Token Service', (): void => { expect(introspection).toMatchObject({ ...grant, access: [access], - key: { proof: 'httpsig', jwk: testJwk }, + key: { proof: 'httpsig', jwk: testClientKey }, clientId }) scope.isDone() @@ -194,7 +203,7 @@ describe('Access Token Service', (): void => { beforeEach(async (): Promise => { grant = await Grant.query(trx).insertAndFetch({ ...BASE_GRANT, - clientKeyId: testJwk.kid, + clientKeyId: testKeys.keyId, continueToken: crypto.randomBytes(8).toString('hex').toUpperCase(), continueId: v4(), interactId: v4(), @@ -241,7 +250,7 @@ describe('Access Token Service', (): void => { beforeEach(async (): Promise => { grant = await Grant.query(trx).insertAndFetch({ ...BASE_GRANT, - clientKeyId: testJwk.kid, + clientKeyId: testKeys.keyId, continueToken: crypto.randomBytes(8).toString('hex').toUpperCase(), continueId: v4(), interactId: v4(), diff --git a/packages/auth/src/accessToken/service.ts b/packages/auth/src/accessToken/service.ts index a445cbb3c2..69f13a1966 100644 --- a/packages/auth/src/accessToken/service.ts +++ b/packages/auth/src/accessToken/service.ts @@ -1,10 +1,11 @@ import * as crypto from 'crypto' import { v4 } from 'uuid' import { Transaction, TransactionOrKnex } from 'objection' +import { JWK } from 'http-signature-utils' import { BaseService } from '../shared/baseService' import { Grant, GrantState } from '../grant/model' -import { ClientService, JWKWithRequired } from '../client/service' +import { ClientService } from '../client/service' import { AccessToken } from './model' import { IAppConfig } from '../config/app' import { Access } from '../access/model' @@ -26,7 +27,7 @@ interface ServiceDependencies extends BaseService { export interface KeyInfo { proof: string - jwk: JWKWithRequired + jwk: JWK } export interface Introspection extends Partial { diff --git a/packages/auth/src/client/service.test.ts b/packages/auth/src/client/service.test.ts index e6ad56a9b4..50599d1f65 100644 --- a/packages/auth/src/client/service.test.ts +++ b/packages/auth/src/client/service.test.ts @@ -1,6 +1,6 @@ import { faker } from '@faker-js/faker' import nock from 'nock' -import { JWK } from 'open-payments' +import { JWK, generateJwk, generateTestKeys } from 'http-signature-utils' import { createTestApp, TestContainer } from '../tests/app' import { Config } from '../config/app' @@ -8,7 +8,6 @@ import { IocContract } from '@adonisjs/fold' import { initIocContainer } from '../' import { AppServices } from '../app' import { ClientService } from './service' -import { generateTestKeys } from '../tests/signature' const TEST_CLIENT_DISPLAY = { name: 'Test Client', @@ -78,8 +77,8 @@ describe('Client Service', (): void => { beforeAll(async (): Promise => { for (let i = 0; i < 3; i++) { - const { publicKey } = await generateTestKeys() - keys.push(publicKey) + const { privateKey, keyId } = await generateTestKeys() + keys.push(generateJwk({ privateKey, keyId })) } }) diff --git a/packages/auth/src/client/service.ts b/packages/auth/src/client/service.ts index 7c4da94aff..b18a151dc1 100644 --- a/packages/auth/src/client/service.ts +++ b/packages/auth/src/client/service.ts @@ -1,21 +1,10 @@ -import { JWK as JoseWk } from 'jose' -import { JWK, UnauthenticatedClient } from 'open-payments' +import { UnauthenticatedClient } from 'open-payments' +import { JWK } from 'http-signature-utils' import { BaseService } from '../shared/baseService' -export interface JWKWithRequired extends JoseWk { - kid: string - x: string - alg: string - kty: string - crv: string - exp?: number - nbf?: number - revoked?: boolean -} - export interface ClientKey { - jwk: JWKWithRequired + jwk: JWK client: ClientDetails } diff --git a/packages/auth/src/grant/routes.test.ts b/packages/auth/src/grant/routes.test.ts index 2e1fd6ca12..f6731bbf83 100644 --- a/packages/auth/src/grant/routes.test.ts +++ b/packages/auth/src/grant/routes.test.ts @@ -10,7 +10,7 @@ import { URL } from 'url' import { createContext as createAppContext } from '../tests/context' import { createTestApp, TestContainer } from '../tests/app' import { Config, IAppConfig } from '../config/app' -import { initIocContainer, JWKWithRequired } from '..' +import { initIocContainer } from '..' import { AppServices } from '../app' import { truncateTables } from '../tests/tableManager' import { GrantRoutes, GrantChoices } from './routes' @@ -19,7 +19,7 @@ import { Access } from '../access/model' import { Grant, StartMethod, FinishMethod, GrantState } from '../grant/model' import { AccessToken } from '../accessToken/model' import { AccessTokenService } from '../accessToken/service' -import { generateTestKeys } from '../tests/signature' +import { generateTestKeys } from 'http-signature-utils' import { KEY_REGISTRY_ORIGIN } from '../tests/signature' export { KEY_REGISTRY_ORIGIN } from '../tests/signature' @@ -86,7 +86,7 @@ describe('Grant Routes', (): void => { let grantRoutes: GrantRoutes let config: IAppConfig let accessTokenService: AccessTokenService - let clientKey: JWKWithRequired + let clientKeyId: string let grant: Grant @@ -99,7 +99,7 @@ describe('Grant Routes', (): void => { finishUri: 'https://example.com', clientNonce: crypto.randomBytes(8).toString('hex').toUpperCase(), client: CLIENT, - clientKeyId: clientKey.kid, + clientKeyId: clientKeyId, interactId: v4(), interactRef: v4(), interactNonce: crypto.randomBytes(8).toString('hex').toUpperCase() @@ -110,7 +110,7 @@ describe('Grant Routes', (): void => { params: Record ) => { const ctx = createAppContext(reqOpts, params) - ctx.clientKeyId = clientKey.kid + ctx.clientKeyId = clientKeyId return ctx } @@ -133,8 +133,7 @@ describe('Grant Routes', (): void => { jestOpenAPI(openApi.authServerSpec) accessTokenService = await deps.use('accessTokenService') - const { publicKey } = await generateTestKeys() - clientKey = publicKey + clientKeyId = (await generateTestKeys()).keyId }) afterEach(async (): Promise => { diff --git a/packages/auth/src/grant/service.test.ts b/packages/auth/src/grant/service.test.ts index 4a59b2740f..41de84a175 100644 --- a/packages/auth/src/grant/service.test.ts +++ b/packages/auth/src/grant/service.test.ts @@ -13,8 +13,7 @@ import { GrantService, GrantRequest } from '../grant/service' import { Grant, StartMethod, FinishMethod, GrantState } from '../grant/model' import { Action, AccessType } from '../access/types' import { Access } from '../access/model' -import { JWKWithRequired } from '../client/service' -import { generateTestKeys } from '../tests/signature' +import { generateTestKeys } from 'http-signature-utils' describe('Grant Service', (): void => { let deps: IocContract @@ -22,7 +21,7 @@ describe('Grant Service', (): void => { let grantService: GrantService let knex: Knex let trx: Knex.Transaction - let testJwk: JWKWithRequired + let testKeyId: string let grant: Grant @@ -32,8 +31,7 @@ describe('Grant Service', (): void => { knex = await deps.use('knex') appContainer = await createTestApp(deps) - const { publicKey } = await generateTestKeys() - testJwk = publicKey + testKeyId = (await generateTestKeys()).keyId }) const CLIENT = faker.internet.url() @@ -48,7 +46,7 @@ describe('Grant Service', (): void => { finishUri: 'https://example.com', clientNonce: crypto.randomBytes(8).toString('hex').toUpperCase(), client: CLIENT, - clientKeyId: testJwk.kid, + clientKeyId: testKeyId, interactId: v4(), interactRef: v4(), interactNonce: crypto.randomBytes(8).toString('hex').toUpperCase() @@ -90,7 +88,7 @@ describe('Grant Service', (): void => { test('Can initiate a grant', async (): Promise => { const grantRequest: GrantRequest = { ...BASE_GRANT_REQUEST, - clientKeyId: testJwk.kid, + clientKeyId: testKeyId, access_token: { access: [ { @@ -114,7 +112,7 @@ describe('Grant Service', (): void => { finishUri: BASE_GRANT_REQUEST.interact.finish.uri, clientNonce: BASE_GRANT_REQUEST.interact.finish.nonce, client: CLIENT, - clientKeyId: testJwk.kid, + clientKeyId: testKeyId, startMethod: expect.arrayContaining([StartMethod.Redirect]) }) @@ -131,7 +129,7 @@ describe('Grant Service', (): void => { test('Can issue a grant without interaction', async (): Promise => { const grantRequest: GrantRequest = { ...BASE_GRANT_REQUEST, - clientKeyId: testJwk.kid, + clientKeyId: testKeyId, access_token: { access: [ { @@ -149,7 +147,7 @@ describe('Grant Service', (): void => { state: GrantState.Granted, continueId: expect.any(String), continueToken: expect.any(String), - clientKeyId: testJwk.kid + clientKeyId: testKeyId }) await expect( diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 78dfeca29a..3e769dd848 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -17,8 +17,6 @@ import { createOpenAPI } from 'openapi' import { createUnauthenticatedClient as createOpenPaymentsClient } from 'open-payments' export { KeyInfo } from './accessToken/service' -export { JWKWithRequired } from './client/service' -export { HttpSigContext, verifySigAndChallenge } from './signature/middleware' const container = initIocContainer(Config) const app = new App(container) diff --git a/packages/auth/src/signature/middleware.test.ts b/packages/auth/src/signature/middleware.test.ts index 878e6fbb60..3e94e7e994 100644 --- a/packages/auth/src/signature/middleware.test.ts +++ b/packages/auth/src/signature/middleware.test.ts @@ -1,10 +1,14 @@ import crypto from 'crypto' import nock from 'nock' import { faker } from '@faker-js/faker' -import { importJWK } from 'jose' import { v4 } from 'uuid' import { Knex } from 'knex' -import { createContentDigestHeader } from 'httpbis-digest-headers' +import { + JWK, + generateTestKeys, + TestKeys, + generateJwk +} from 'http-signature-utils' import { createTestApp, TestContainer } from '../tests/app' import { truncateTables } from '../tests/tableManager' @@ -12,16 +16,12 @@ import { Config } from '../config/app' import { IocContract } from '@adonisjs/fold' import { initIocContainer } from '../' import { AppServices } from '../app' -import { JWKWithRequired } from '../client/service' import { createContext, createContextWithSigHeaders } from '../tests/context' -import { generateTestKeys } from '../tests/signature' import { Grant, GrantState, StartMethod, FinishMethod } from '../grant/model' import { Access } from '../access/model' import { AccessToken } from '../accessToken/model' import { AccessType, Action } from '../access/types' import { - verifySig, - sigInputToChallenge, tokenHttpsigMiddleware, grantContinueHttpsigMiddleware, grantInitiationHttpsigMiddleware @@ -32,16 +32,18 @@ describe('Signature Service', (): void => { let appContainer: TestContainer const CLIENT = faker.internet.url() - let privateKey: JWKWithRequired - let testClientKey: JWKWithRequired + let testKeys: TestKeys + let testClientKey: JWK beforeAll(async (): Promise => { deps = await initIocContainer(Config) appContainer = await createTestApp(deps) - const keys = await generateTestKeys() - privateKey = keys.privateKey - testClientKey = keys.publicKey + testKeys = await generateTestKeys() + testClientKey = generateJwk({ + privateKey: testKeys.privateKey, + keyId: testKeys.keyId + }) }) afterAll(async (): Promise => { @@ -49,113 +51,6 @@ describe('Signature Service', (): void => { appContainer.shutdown() }) - describe('signatures', (): void => { - test('can verify a signature', async (): Promise => { - const challenge = 'test-challenge' - const privateJwk = (await importJWK(privateKey)) as crypto.KeyLike - const signature = crypto.sign(null, Buffer.from(challenge), privateJwk) - await expect( - verifySig(signature.toString('base64'), testClientKey, challenge) - ).resolves.toBe(true) - }) - - const testRequestBody = { foo: 'bar' } - - test.each` - title | withAuthorization | withRequestBody - ${''} | ${true} | ${true} - ${' without an authorization header'} | ${false} | ${true} - ${' without a request body'} | ${true} | ${false} - `( - 'can construct a challenge from signature input$title', - ({ withAuthorization, withRequestBody }): void => { - let sigInputHeader = 'sig1=("@method" "@target-uri" "content-type"' - - const headers = { - 'Content-Type': 'application/json' - } - let expectedChallenge = `"@method": GET\n"@target-uri": http://example.com/test\n"content-type": application/json\n` - const contentDigest = createContentDigestHeader( - JSON.stringify(testRequestBody), - ['sha-512'] - ) - - if (withRequestBody) { - sigInputHeader += ' "content-digest" "content-length"' - headers['Content-Digest'] = contentDigest - headers['Content-Length'] = '1234' - expectedChallenge += `"content-digest": ${contentDigest}\n"content-length": 1234\n` - } - - if (withAuthorization) { - sigInputHeader += ' "authorization"' - headers['Authorization'] = 'GNAP test-access-token' - expectedChallenge += '"authorization": GNAP test-access-token\n' - } - - sigInputHeader += ');created=1618884473;keyid="gnap-key"' - headers['Signature-Input'] = sigInputHeader - expectedChallenge += `"@signature-params": ${sigInputHeader.replace( - 'sig1=', - '' - )}` - - const ctx = createContext( - { - headers, - method: 'GET', - url: 'example.com/test' - }, - {}, - deps - ) - - ctx.request.body = withRequestBody ? testRequestBody : {} - - const challenge = sigInputToChallenge(sigInputHeader, ctx) - - expect(challenge).toEqual(expectedChallenge) - } - ) - - test.each` - title | sigInputHeader - ${'fails if a component is not in lower case'} | ${'sig1=("@METHOD" "@target-uri" "content-digest" "content-length" "content-type" "authorization");created=1618884473;keyid="gnap-key"'} - ${'fails @method is missing'} | ${'sig1=("@target-uri" "content-digest" "content-length" "content-type");created=1618884473;keyid="gnap-key"'} - ${'fails if @target-uri is missing'} | ${'sig1=("@method" "content-digest" "content-length" "content-type");created=1618884473;keyid="gnap-key"'} - ${'fails if @content-digest is missing while body is present'} | ${'sig1=("@method" "@target-uri" "content-length" "content-type");created=1618884473;keyid="gnap-key"'} - ${'fails if authorization header is present in headers but not in signature input'} | ${'sig1=("@method" "@target-uri" "content-digest" "content-length" "content-type");created=1618884473;keyid="gnap-key"'} - `( - 'constructs signature input and $title', - async ({ sigInputHeader }): Promise => { - const ctx = createContext( - { - headers: { - 'Content-Type': 'application/json', - 'Content-Digest': createContentDigestHeader( - JSON.stringify(testRequestBody), - ['sha-512'] - ), - 'Content-Length': '1234', - 'Signature-Input': sigInputHeader, - Authorization: 'GNAP test-access-token' - }, - method: 'GET', - url: '/test' - }, - {}, - deps - ) - - ctx.request.body = testRequestBody - ctx.method = 'GET' - ctx.request.url = '/test' - - expect(sigInputToChallenge(sigInputHeader, ctx)).toBe(null) - } - ) - }) - describe('Signature middleware', (): void => { let grant: Grant let token: AccessToken @@ -207,7 +102,7 @@ describe('Signature Service', (): void => { beforeEach(async (): Promise => { grant = await Grant.query(trx).insertAndFetch({ ...BASE_GRANT, - clientKeyId: testClientKey.kid + clientKeyId: testKeys.keyId }) await Access.query(trx).insertAndFetch({ grantId: grant.id, @@ -250,15 +145,15 @@ describe('Signature Service', (): void => { { client: CLIENT }, - privateKey, - testClientKey.kid, + testKeys.privateKey, + testKeys.keyId, deps ) await grantInitiationHttpsigMiddleware(ctx, next) expect(ctx.response.status).toEqual(200) - expect(ctx.clientKeyId).toEqual(testClientKey.kid) + expect(ctx.clientKeyId).toEqual(testKeys.keyId) expect(next).toHaveBeenCalled() scope.done() @@ -282,14 +177,14 @@ describe('Signature Service', (): void => { }, { id: grant.continueId }, { interact_ref: grant.interactRef }, - privateKey, - testClientKey.kid, + testKeys.privateKey, + testKeys.keyId, deps ) await grantContinueHttpsigMiddleware(ctx, next) expect(ctx.response.status).toEqual(200) - expect(ctx.clientKeyId).toEqual(testClientKey.kid) + expect(ctx.clientKeyId).toEqual(testKeys.keyId) expect(next).toHaveBeenCalled() scope.done() @@ -316,8 +211,8 @@ describe('Signature Service', (): void => { proof: 'httpsig', resource_server: 'test' }, - privateKey, - testClientKey.kid, + testKeys.privateKey, + testKeys.keyId, deps ) @@ -325,7 +220,7 @@ describe('Signature Service', (): void => { expect(next).toHaveBeenCalled() expect(ctx.response.status).toEqual(200) - expect(ctx.clientKeyId).toEqual(testClientKey.kid) + expect(ctx.clientKeyId).toEqual(testKeys.keyId) scope.done() }) @@ -375,8 +270,8 @@ describe('Signature Service', (): void => { }, { id: grant.continueId }, { interact_ref: grant.interactRef }, - privateKey, - testClientKey.kid, + testKeys.privateKey, + testKeys.keyId, deps ) @@ -402,8 +297,8 @@ describe('Signature Service', (): void => { { client: CLIENT }, - privateKey, - testClientKey.kid, + testKeys.privateKey, + testKeys.keyId, deps ) @@ -428,8 +323,8 @@ describe('Signature Service', (): void => { { client: CLIENT }, - privateKey, - testClientKey.kid, + testKeys.privateKey, + testKeys.keyId, deps ) diff --git a/packages/auth/src/signature/middleware.ts b/packages/auth/src/signature/middleware.ts index 9f25a096cc..73eb0c2bdb 100644 --- a/packages/auth/src/signature/middleware.ts +++ b/packages/auth/src/signature/middleware.ts @@ -1,51 +1,26 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import * as crypto from 'crypto' -import { importJWK } from 'jose' -import { JWK } from 'open-payments' -import { verifyContentDigest } from 'httpbis-digest-headers' +import { + validateSignature, + validateSignatureHeaders, + RequestLike +} from 'http-signature-utils' import { AppContext } from '../app' import { Grant } from '../grant/model' -import { Context } from 'koa' -export async function verifySig( - sig: string, - jwk: JWK, - challenge: string -): Promise { - const publicKey = (await importJWK(jwk)) as crypto.KeyLike - const data = Buffer.from(challenge) - return crypto.verify(null, data, publicKey, Buffer.from(sig, 'base64')) -} - -export async function verifySigAndChallenge( - clientKey: JWK, - ctx: HttpSigContext -): Promise { - const sig = ctx.headers['signature'] as string - const sigInput = ctx.headers['signature-input'] as string - const challenge = sigInputToChallenge(sigInput, ctx) - if (!challenge) { - ctx.throw(400, 'invalid signature input', { error: 'invalid_request' }) - } - - const verified = await verifySig( - sig.replace('sig1=', ''), - clientKey, - challenge - ) - - if (verified) { - return true - } else { - ctx.throw(401, 'invalid signature') +function contextToRequestLike(ctx: AppContext): RequestLike { + return { + url: ctx.href, + method: ctx.method, + headers: ctx.headers, + body: ctx.request.body ? JSON.stringify(ctx.request.body) : undefined } } async function verifySigFromClient( client: string, - ctx: HttpSigContext + ctx: AppContext ): Promise { const clientService = await ctx.container.use('clientService') const clientKey = await clientService.getKey({ @@ -56,13 +31,12 @@ async function verifySigFromClient( if (!clientKey) { ctx.throw(400, 'invalid client', { error: 'invalid_client' }) } - - return verifySigAndChallenge(clientKey, ctx) + return validateSignature(clientKey, contextToRequestLike(ctx)) } async function verifySigFromBoundKey( grant: Grant, - ctx: HttpSigContext + ctx: AppContext ): Promise { const sigInput = ctx.headers['signature-input'] as string ctx.clientKeyId = getSigInputKeyId(sigInput) @@ -73,18 +47,6 @@ async function verifySigFromBoundKey( return verifySigFromClient(grant.client, ctx) } -// TODO: Replace with public httpsig library -function getSigInputComponents(sigInput: string): string[] | null { - // https://datatracker.ietf.org/doc/html/rfc8941#section-4.1.1.1 - const messageComponents = sigInput - .split('sig1=')[1] - ?.split(';')[0] - ?.split(' ') - return messageComponents - ? messageComponents.map((component) => component.replace(/[()"]/g, '')) - : null -} - const KEY_ID_PREFIX = 'keyid="' function getSigInputKeyId(sigInput: string): string | undefined { @@ -95,99 +57,11 @@ function getSigInputKeyId(sigInput: string): string | undefined { return keyIdParam?.slice(KEY_ID_PREFIX.length, -1) } -function validateSigInputComponents( - sigInputComponents: string[], - ctx: Context -): boolean { - // https://datatracker.ietf.org/doc/html/draft-ietf-gnap-core-protocol#section-7.3.1 - - for (const component of sigInputComponents) { - // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures-09#section-2.1 - if (component !== component.toLowerCase()) return false - } - - const isValidContentDigest = - !sigInputComponents.includes('content-digest') || - (!!ctx.headers['content-digest'] && - ctx.request.body && - Object.keys(ctx.request.body).length > 0 && - sigInputComponents.includes('content-digest') && - verifyContentDigest( - JSON.stringify(ctx.request.body), - ctx.headers['content-digest'] as string - )) - - return !( - !isValidContentDigest || - !sigInputComponents.includes('@method') || - !sigInputComponents.includes('@target-uri') || - (ctx.headers['authorization'] && - !sigInputComponents.includes('authorization')) - ) -} - -export function sigInputToChallenge( - sigInput: string, - ctx: Context -): string | null { - const sigInputComponents = getSigInputComponents(sigInput) - - if ( - !sigInputComponents || - !validateSigInputComponents(sigInputComponents, ctx) - ) - return null - - // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures-09#section-2.3 - let signatureBase = '' - for (const component of sigInputComponents) { - if (component === '@method') { - signatureBase += `"@method": ${ctx.request.method}\n` - } else if (component === '@target-uri') { - signatureBase += `"@target-uri": ${ctx.request.href}\n` - } else { - signatureBase += `"${component}": ${ctx.headers[component]}\n` - } - } - - signatureBase += `"@signature-params": ${( - ctx.headers['signature-input'] as string - )?.replace('sig1=', '')}` - return signatureBase -} - -type HttpSigHeaders = Record<'signature' | 'signature-input', string> - -type HttpSigRequest = Omit & { - headers: HttpSigHeaders -} - -export type HttpSigContext = Context & { - request: HttpSigRequest - headers: HttpSigHeaders -} - -function validateHttpSigHeaders(ctx: Context): ctx is HttpSigContext { - const sig = ctx.headers['signature'] - const sigInput = ctx.headers['signature-input'] as string - - const sigInputComponents = getSigInputComponents(sigInput ?? '') - if ( - !sigInputComponents || - !validateSigInputComponents(sigInputComponents, ctx) - ) - return false - - return ( - sig && sigInput && typeof sig === 'string' && typeof sigInput === 'string' - ) -} - export async function grantContinueHttpsigMiddleware( ctx: AppContext, next: () => Promise ): Promise { - if (!validateHttpSigHeaders(ctx)) { + if (!validateSignatureHeaders(contextToRequestLike(ctx))) { ctx.throw(400, 'invalid signature headers', { error: 'invalid_request' }) } @@ -222,7 +96,10 @@ export async function grantContinueHttpsigMiddleware( return } - await verifySigFromBoundKey(grant, ctx) + const sigVerified = await verifySigFromBoundKey(grant, ctx) + if (!sigVerified) { + ctx.throw(401, 'invalid signature') + } await next() } @@ -230,7 +107,7 @@ export async function grantInitiationHttpsigMiddleware( ctx: AppContext, next: () => Promise ): Promise { - if (!validateHttpSigHeaders(ctx)) { + if (!validateSignatureHeaders(contextToRequestLike(ctx))) { ctx.throw(400, 'invalid signature headers', { error: 'invalid_request' }) } @@ -242,7 +119,10 @@ export async function grantInitiationHttpsigMiddleware( ctx.throw(401, 'invalid signature input', { error: 'invalid_request' }) } - await verifySigFromClient(body.client, ctx) + const sigVerified = await verifySigFromClient(body.client, ctx) + if (!sigVerified) { + ctx.throw(401, 'invalid signature') + } await next() } @@ -250,7 +130,7 @@ export async function tokenHttpsigMiddleware( ctx: AppContext, next: () => Promise ): Promise { - if (!validateHttpSigHeaders(ctx)) { + if (!validateSignatureHeaders(contextToRequestLike(ctx))) { ctx.throw(400, 'invalid signature headers', { error: 'invalid_request' }) } @@ -269,6 +149,10 @@ export async function tokenHttpsigMiddleware( const grantService = await ctx.container.use('grantService') const grant = await grantService.get(accessToken.grantId) - await verifySigFromBoundKey(grant, ctx) + + const sigVerified = await verifySigFromBoundKey(grant, ctx) + if (!sigVerified) { + ctx.throw(401, 'invalid signature') + } await next() } diff --git a/packages/auth/src/tests/context.ts b/packages/auth/src/tests/context.ts index 9d43e45760..5727827df6 100644 --- a/packages/auth/src/tests/context.ts +++ b/packages/auth/src/tests/context.ts @@ -1,12 +1,12 @@ +import crypto from 'crypto' import EventEmitter from 'events' import * as httpMocks from 'node-mocks-http' import Koa from 'koa' import session from 'koa-session' import { IocContract } from '@adonisjs/fold' +import { createHeaders } from 'http-signature-utils' import { AppContext, AppContextData, AppServices } from '../app' -import { generateSigHeaders } from './signature' -import { JWKWithRequired } from '../client/service' export function createContext( reqOpts: httpMocks.RequestOptions, @@ -41,33 +41,29 @@ export async function createContextWithSigHeaders( reqOpts: httpMocks.RequestOptions, params: Record, requestBody: Record, - privateKey: JWKWithRequired, + privateKey: crypto.KeyObject, keyId: string, container?: IocContract ): Promise { const { headers, url, method } = reqOpts - const { signature, sigInput, contentDigest, contentLength, contentType } = - await generateSigHeaders({ - privateKey, - keyId, - url, - method, - optionalComponents: { - body: requestBody, - authorization: headers.Authorization as string - } - }) + const request = { + url, + method, + headers, + body: JSON.stringify(requestBody) + } + const sigHeaders = await createHeaders({ + request, + privateKey, + keyId + }) const ctx = createContext( { ...reqOpts, headers: { ...headers, - 'Content-Digest': contentDigest, - Signature: signature, - 'Signature-Input': sigInput, - 'Content-Type': contentType, - 'Content-Length': contentLength + ...sigHeaders } }, params, diff --git a/packages/auth/src/tests/signature.ts b/packages/auth/src/tests/signature.ts index c9c02d848b..8f8ad7fecd 100644 --- a/packages/auth/src/tests/signature.ts +++ b/packages/auth/src/tests/signature.ts @@ -1,8 +1,4 @@ -import crypto from 'crypto' import { v4 } from 'uuid' -import { createContentDigestHeader } from 'httpbis-digest-headers' -import { importJWK, exportJWK } from 'jose' -import { JWKWithRequired } from '../client/service' export const SIGNATURE_METHOD = 'GET' export const SIGNATURE_TARGET_URI = '/test' @@ -20,98 +16,3 @@ export const TEST_CLIENT_DISPLAY = { name: TEST_CLIENT.name, uri: TEST_CLIENT.uri } - -// TODO: refactor any oustanding key-using tests to generate them from here -const BASE_TEST_KEY_JWK = { - kty: 'OKP', - alg: 'EdDSA', - crv: 'Ed25519', - use: 'sig' -} - -export async function generateTestKeys(): Promise<{ - keyId: string - publicKey: JWKWithRequired - privateKey: JWKWithRequired -}> { - const { privateKey } = crypto.generateKeyPairSync('ed25519') - - const { x, d } = await exportJWK(privateKey) - const keyId = v4() - return { - keyId, - publicKey: { - ...BASE_TEST_KEY_JWK, - kid: keyId, - x - }, - privateKey: { - ...BASE_TEST_KEY_JWK, - kid: keyId, - x, - d - } - } -} - -export async function generateSigHeaders({ - privateKey, - url, - method, - keyId, - optionalComponents -}: { - privateKey: JWKWithRequired - url: string - method: string - keyId: string - optionalComponents?: { - body?: unknown - authorization?: string - } -}): Promise<{ - sigInput: string - signature: string - contentDigest?: string - contentLength?: string - contentType?: string -}> { - let sigInputComponents = 'sig1=("@method" "@target-uri"' - const { body, authorization } = optionalComponents ?? {} - if (body) - sigInputComponents += ' "content-digest" "content-length" "content-type"' - - if (authorization) sigInputComponents += ' "authorization"' - - const sigInput = sigInputComponents + `);created=1618884473;keyid="${keyId}"` - let challenge = `"@method": ${method}\n"@target-uri": ${url}\n` - let contentDigest - let contentLength - let contentType - if (body) { - contentDigest = createContentDigestHeader(JSON.stringify(body), ['sha-512']) - challenge += `"content-digest": ${contentDigest}\n` - - contentLength = Buffer.from(JSON.stringify(body), 'utf-8').length - challenge += `"content-length": ${contentLength}\n` - contentType = 'application/json' - challenge += `"content-type": ${contentType}\n` - } - - if (authorization) { - challenge += `"authorization": ${authorization}\n` - } - - challenge += `"@signature-params": ${sigInput.replace('sig1=', '')}` - - const privateJwk = (await importJWK(privateKey)) as crypto.KeyLike - const signature = crypto.sign(null, Buffer.from(challenge), privateJwk) - - return { - signature: signature.toString('base64'), - sigInput, - contentDigest, - contentLength, - contentType - } -} diff --git a/packages/backend/package.json b/packages/backend/package.json index 4b3a588bf1..bb13466d84 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -4,7 +4,7 @@ "test": "jest --passWithNoTests --maxWorkers=50%", "knex": "knex", "generate": "graphql-codegen --config codegen.yml", - "build:deps": "pnpm --filter auth build", + "build:deps": "pnpm --filter open-payments build", "build": "pnpm build:deps && tsc --build tsconfig.json && pnpm copy-files", "clean": "rm -fr dist/", "copy-files": "cp src/graphql/schema.graphql dist/graphql/", @@ -28,7 +28,6 @@ "@types/tmp": "^0.2.3", "@types/uuid": "^8.3.4", "apollo-server": "^3.10.1", - "auth": "workspace:../auth", "cross-fetch": "^3.1.4", "ilp-protocol-ildcp": "^2.2.3", "ilp-protocol-stream": "^2.7.1", @@ -67,6 +66,7 @@ "graphql": "^16.6.0", "graphql-scalars": "^1.18.0", "graphql-tools": "^8.3.3", + "http-signature-utils": "workspace:../http-signature-utils", "ilp-packet": "3.1.4-alpha.1", "ilp-protocol-ccp": "^1.2.2", "ilp-protocol-ildcp": "^2.2.3", diff --git a/packages/backend/src/config/app.ts b/packages/backend/src/config/app.ts index 3aaafe4258..f875621040 100644 --- a/packages/backend/src/config/app.ts +++ b/packages/backend/src/config/app.ts @@ -1,6 +1,7 @@ import * as crypto from 'crypto' import * as fs from 'fs' import { ConnectionOptions } from 'tls' +import { parseOrProvisionKey } from 'http-signature-utils' function envString(name: string, value: string): string { const envValue = process.env[name] @@ -24,9 +25,6 @@ function envBool(name: string, value: boolean): boolean { export type IAppConfig = typeof Config -const TMP_DIR = './tmp' -const PRIVATE_KEY_FILE = `${TMP_DIR}/private-key-${new Date().getTime()}.pem` - export const Config = { logLevel: envString('LOG_LEVEL', 'info'), // publicHost is for open payments URLs. @@ -139,32 +137,3 @@ function parseRedisTlsConfig( return Object.keys(options).length > 0 ? options : undefined } - -// exported for testing -export function parseOrProvisionKey( - keyFile: string | undefined -): crypto.KeyObject { - if (keyFile) { - try { - const key = crypto.createPrivateKey(fs.readFileSync(keyFile)) - const jwk = key.export({ format: 'jwk' }) - if (jwk.crv === 'Ed25519') { - return key - } else { - console.log('Private key is not EdDSA-Ed25519 key. Generating new key.') - } - } catch (err) { - console.log('Private key could not be loaded.') - throw err - } - } - const keypair = crypto.generateKeyPairSync('ed25519') - if (!fs.existsSync(TMP_DIR)) { - fs.mkdirSync(TMP_DIR) - } - fs.writeFileSync( - PRIVATE_KEY_FILE, - keypair.privateKey.export({ format: 'pem', type: 'pkcs8' }) - ) - return keypair.privateKey -} diff --git a/packages/backend/src/open_payments/auth/middleware.test.ts b/packages/backend/src/open_payments/auth/middleware.test.ts index 390a0bb635..6999cf8f74 100644 --- a/packages/backend/src/open_payments/auth/middleware.test.ts +++ b/packages/backend/src/open_payments/auth/middleware.test.ts @@ -2,6 +2,15 @@ import assert from 'assert' import nock, { Definition } from 'nock' import { URL } from 'url' import { v4 as uuid } from 'uuid' +import { Context } from 'koa' +import { + generateTestKeys, + JWK, + createHeaders, + Headers, + TestKeys, + generateJwk +} from 'http-signature-utils' import { createAuthMiddleware } from './middleware' import { GrantJSON, AccessType, AccessAction } from './grant' @@ -15,14 +24,9 @@ import { createTestApp, TestContainer } from '../../tests/app' import { createPaymentPointer } from '../../tests/paymentPointer' import { truncateTables } from '../../tests/tableManager' import { setup, SetupOptions } from '../payment_pointer/model.test' -import { HttpSigContext, JWKWithRequired, KeyInfo } from 'auth' -import { generateTestKeys, generateSigHeaders } from 'auth/src/tests/signature' -import { TokenInfo, TokenInfoJSON } from './service' +import { KeyInfo, TokenInfo, TokenInfoJSON } from './service' -type AppMiddleware = ( - ctx: HttpSigContext, - next: () => Promise -) => Promise +type AppMiddleware = (ctx: Context, next: () => Promise) => Promise type IntrospectionBody = { access_token: string @@ -34,29 +38,21 @@ describe('Auth Middleware', (): void => { let appContainer: TestContainer let authServerIntrospectionUrl: URL let middleware: AppMiddleware - let ctx: HttpSigContext + let ctx: Context let next: jest.MockedFunction<() => Promise> let validateRequest: RequestValidator let mockKeyInfo: KeyInfo const token = 'OS9M2PMHKUR64TB8N6BW7OZB8CDFONP219RP1LT0' - let generatedKeyPair: { - keyId: string - publicKey: JWKWithRequired - privateKey: JWKWithRequired - } + let testKeys: TestKeys let requestPath: string let requestAuthorization: string let requestBody: Body let requestUrl: string let requestMethod: RequestMethod - let requestSignatureHeaders: { - sigInput: string - signature: string - contentDigest?: string - } - let requestJwk: JWKWithRequired + let requestSignatureHeaders: Headers + let requestJwk: JWK - function setupHttpSigContext(options: SetupOptions): HttpSigContext { + function setupHttpSigContext(options: SetupOptions): Context { const context = setup(options) if ( !context.headers['signature'] || @@ -75,31 +71,28 @@ describe('Auth Middleware', (): void => { } async function prepareTest(includeBody: boolean) { - requestSignatureHeaders = await generateSigHeaders({ - privateKey: generatedKeyPair.privateKey, - keyId: generatedKeyPair.keyId, + const request = { url: requestUrl, method: requestMethod, - optionalComponents: { - body: includeBody ? requestBody : undefined, - authorization: requestAuthorization - } + headers: { authorization: requestAuthorization }, + body: includeBody ? JSON.stringify(requestBody) : undefined + } + requestSignatureHeaders = await createHeaders({ + request, + privateKey: testKeys.privateKey, + keyId: testKeys.keyId + }) + requestJwk = generateJwk({ + privateKey: testKeys.privateKey, + keyId: testKeys.keyId }) - requestJwk = generatedKeyPair.publicKey ctx = setupHttpSigContext({ reqOpts: { headers: { Accept: 'application/json', Authorization: `GNAP ${token}`, - Signature: `sig1=:${requestSignatureHeaders.signature}:`, - 'Signature-Input': requestSignatureHeaders.sigInput, - 'Content-Digest': includeBody - ? requestSignatureHeaders.contentDigest - : undefined, - 'Content-Length': includeBody - ? JSON.stringify(requestBody).length.toString() - : undefined + ...requestSignatureHeaders }, method: requestMethod, body: includeBody ? requestBody : undefined, @@ -129,7 +122,7 @@ describe('Auth Middleware', (): void => { path: requestPath, method: HttpMethod.POST }) - generatedKeyPair = await generateTestKeys() + testKeys = await generateTestKeys() requestMethod = HttpMethod.POST.toUpperCase() as RequestMethod requestBody = { access_token: token, @@ -344,7 +337,7 @@ describe('Auth Middleware', (): void => { Accept: 'application/json', Authorization: `GNAP ${token}`, Signature: 'aaaaaaaaaa=', - 'Signature-Input': requestSignatureHeaders.sigInput + 'Signature-Input': requestSignatureHeaders['Signature-Input'] }, method: requestMethod, url: requestUrl @@ -382,8 +375,8 @@ describe('Auth Middleware', (): void => { Accept: 'application/json', Authorization: `GNAP ${token}`, Signature: 'aaaaaaaaaa=', - 'Signature-Input': requestSignatureHeaders.sigInput, - 'Content-Digest': requestSignatureHeaders.contentDigest, + 'Signature-Input': requestSignatureHeaders['Signature-Input'], + 'Content-Digest': requestSignatureHeaders['Content-Digest'], 'Content-Length': JSON.stringify(requestBody).length.toString() }, method: requestMethod, @@ -482,9 +475,12 @@ describe('Auth Middleware', (): void => { mockKeyInfo ) const scope = mockAuthServer(grant.toJSON()) - ctx.request.headers['signature-input'] = ctx.request.headers[ - 'signature-input' - ].replace('gnap-key', 'mismatched-key') + let sigInput = ctx.request.headers['signature-input'] as string + sigInput = sigInput.replace( + /(keyid=")[0-9a-z-]{36}/g, + '$1' + 'mismatched-key' + ) + ctx.request.headers['signature-input'] = sigInput await expect(middleware(ctx, next)).resolves.toBeUndefined() expect(ctx.status).toBe(401) expect(next).not.toHaveBeenCalled() @@ -508,9 +504,12 @@ describe('Auth Middleware', (): void => { mockKeyInfo ) const scope = mockAuthServer(grant.toJSON()) - ctx.request.headers['signature-input'] = ctx.request.headers[ - 'signature-input' - ].replace('gnap-key', 'mismatched-key') + let sigInput = ctx.request.headers['signature-input'] as string + sigInput = sigInput.replace( + /(keyid=")[0-9a-z-]{36}/g, + '$1' + 'mismatched-key' + ) + ctx.request.headers['signature-input'] = sigInput await expect(middleware(ctx, next)).resolves.toBeUndefined() expect(ctx.status).toBe(401) expect(next).not.toHaveBeenCalled() @@ -523,8 +522,8 @@ describe('Auth Middleware', (): void => { headers: { Accept: 'application/json', Authorization: `GNAP ${token}`, - Signature: `sig1=:${requestSignatureHeaders.signature}:`, - 'Signature-Input': requestSignatureHeaders.sigInput, + Signature: `sig1=:${requestSignatureHeaders['Signature']}:`, + 'Signature-Input': requestSignatureHeaders['Signature-Input'], 'Content-Digest': 'aaaaaaaaaa=', 'Content-Length': JSON.stringify(requestBody).length.toString() }, diff --git a/packages/backend/src/open_payments/auth/middleware.ts b/packages/backend/src/open_payments/auth/middleware.ts index 12791b1e2b..a22c939161 100644 --- a/packages/backend/src/open_payments/auth/middleware.ts +++ b/packages/backend/src/open_payments/auth/middleware.ts @@ -1,6 +1,15 @@ +import { RequestLike, validateSignature } from 'http-signature-utils' import { AccessType, AccessAction } from './grant' -import { HttpSigContext, verifySigAndChallenge } from 'auth' +import { PaymentPointerContext } from '../../app' +function contextToRequestLike(ctx: PaymentPointerContext): RequestLike { + return { + url: ctx.href, + method: ctx.method, + headers: ctx.headers, + body: ctx.request.body ? JSON.stringify(ctx.request.body) : undefined + } +} export function createAuthMiddleware({ type, action @@ -9,7 +18,7 @@ export function createAuthMiddleware({ action: AccessAction }) { return async ( - ctx: HttpSigContext, + ctx: PaymentPointerContext, next: () => Promise ): Promise => { const config = await ctx.container.use('config') @@ -41,7 +50,9 @@ export function createAuthMiddleware({ } if (!config.bypassSignatureValidation) { try { - if (!(await verifySigAndChallenge(grant.key.jwk, ctx))) { + if ( + !(await validateSignature(grant.key.jwk, contextToRequestLike(ctx))) + ) { ctx.throw(401, 'Invalid signature') } } catch (e) { diff --git a/packages/backend/src/open_payments/auth/service.ts b/packages/backend/src/open_payments/auth/service.ts index ddda107729..0d0c252f58 100644 --- a/packages/backend/src/open_payments/auth/service.ts +++ b/packages/backend/src/open_payments/auth/service.ts @@ -1,5 +1,4 @@ import axios from 'axios' -import { KeyInfo } from 'auth' import { Logger } from 'pino' import { @@ -10,6 +9,12 @@ import { GrantAccessJSON } from './grant' import { OpenAPI, HttpMethod, ResponseValidator } from 'openapi' +import { JWK } from 'http-signature-utils' + +export interface KeyInfo { + proof: string + jwk: JWK +} export interface TokenInfoJSON extends GrantJSON { key: KeyInfo diff --git a/packages/backend/src/paymentPointerKey/model.ts b/packages/backend/src/paymentPointerKey/model.ts index 11c593aae8..8fbb725061 100644 --- a/packages/backend/src/paymentPointerKey/model.ts +++ b/packages/backend/src/paymentPointerKey/model.ts @@ -1,6 +1,6 @@ import { BaseModel } from '../shared/baseModel' -import { JWKWithRequired } from 'auth' +import { JWK } from 'http-signature-utils' export class PaymentPointerKey extends BaseModel { public static get tableName(): string { @@ -10,5 +10,5 @@ export class PaymentPointerKey extends BaseModel { public id!: string public paymentPointerId!: string - public jwk!: JWKWithRequired + public jwk!: JWK } diff --git a/packages/backend/src/paymentPointerKey/routes.test.ts b/packages/backend/src/paymentPointerKey/routes.test.ts index d8f3df9beb..4c0f05865c 100644 --- a/packages/backend/src/paymentPointerKey/routes.test.ts +++ b/packages/backend/src/paymentPointerKey/routes.test.ts @@ -1,6 +1,6 @@ import jestOpenAPI from 'jest-openapi' import { Knex } from 'knex' -import { generateJwk } from 'open-payments' +import { generateJwk } from 'http-signature-utils' import { v4 as uuid } from 'uuid' import { createContext } from '../tests/context' @@ -20,7 +20,6 @@ const TEST_KEY = { kty: 'OKP', alg: 'EdDSA', crv: 'Ed25519', - key_ops: ['sign', 'verify'], use: 'sig' } diff --git a/packages/backend/src/paymentPointerKey/routes.ts b/packages/backend/src/paymentPointerKey/routes.ts index 54eb3c232f..2c3910d9cc 100644 --- a/packages/backend/src/paymentPointerKey/routes.ts +++ b/packages/backend/src/paymentPointerKey/routes.ts @@ -1,4 +1,4 @@ -import { generateJwk, JWK } from 'open-payments' +import { generateJwk, JWK } from 'http-signature-utils' import { PaymentPointerContext } from '../app' import { IAppConfig } from '../config/app' diff --git a/packages/backend/src/paymentPointerKey/service.ts b/packages/backend/src/paymentPointerKey/service.ts index 5da8c17578..46290016b2 100644 --- a/packages/backend/src/paymentPointerKey/service.ts +++ b/packages/backend/src/paymentPointerKey/service.ts @@ -2,7 +2,7 @@ import { TransactionOrKnex } from 'objection' import { PaymentPointerKey } from './model' import { BaseService } from '../shared/baseService' -import { JWKWithRequired } from 'auth' +import { JWK } from 'http-signature-utils' export interface PaymentPointerKeyService { create(options: CreateOptions): Promise @@ -37,7 +37,7 @@ export async function createPaymentPointerKeyService({ interface CreateOptions { paymentPointerId: string - jwk: JWKWithRequired + jwk: JWK } async function create( diff --git a/packages/http-signature-utils/Dockerfile b/packages/http-signature-utils/Dockerfile new file mode 100644 index 0000000000..35de887463 --- /dev/null +++ b/packages/http-signature-utils/Dockerfile @@ -0,0 +1,20 @@ +FROM node:16.17.1-slim as builder + +WORKDIR /workspace + +RUN apt update +RUN apt install -y curl xz-utils python3 build-essential + +# version in curl is not the version used. Dependent on the last command +RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7.9.3 + +# pnpm fetch does require only lockfile +COPY pnpm-lock.yaml ./ +RUN pnpm fetch + +ADD . ./ +RUN pnpm install -r --offline + +RUN pnpm --filter http-signature-utils build + +CMD ["node", "./packages/http-signature-utils/dist/app.js"] diff --git a/packages/http-signature-utils/README.md b/packages/http-signature-utils/README.md new file mode 100644 index 0000000000..55c438149b --- /dev/null +++ b/packages/http-signature-utils/README.md @@ -0,0 +1,139 @@ +# HTTP Signature Utils Library + +The Library includes + +- loading Ed25519 keys from file or creating them +- generating JWKs from Ed25519 keys +- creating HTTP signature headers +- validate and verify HTTP signature headers + +Additionally, the package includes an app that generates HTTP digests and signatures for Postman. + +## Local Development + +### Building + +From the monorepo root directory: + +```shell +pnpm --filter http-signature-utils build +``` + +### Testing + +From the monorepo root directory: + +```shell +pnpm --filter http-signature-utils test +``` + +## Usage + +Load or generate a private key + +```ts +const key = parseOrProvisionKey('/PATH/TO/private-key.pem') +``` + +Create JWK from private key + +```ts +const jwk = generateJwk({ + privateKey: key, + keyId: '5cd52c55-05f1-41be-9474-a5c432cd4375' +}) +``` + +Create Signature Headers + +```ts +const signatureHeaders = await createSignatureHeaders({ + request: { + method: 'POST', + url: 'https://example.com', + headers: { + authorization: 'GNAP 123454321' + }, + body: JSON.stringify(body) + } + privateKey: key, + keyId: '5cd52c55-05f1-41be-9474-a5c432cd4375' +}) +``` + +Create Signature and Content Headers + +```ts +const headers = await createHeaders({ + request: { + method: 'POST', + url: 'https://example.com', + headers: { + authorization: 'GNAP 123454321' + }, + body: JSON.stringify(body) + } + privateKey: key, + keyId: '5cd52c55-05f1-41be-9474-a5c432cd4375' +}) +``` + +Validate Signature and Content Headers + +```ts +const isValidHeader = validateSignatureHeaders(request: { + method: 'POST', + url: 'https://example.com', + headers: { + 'content-type': 'application/json', + 'content-length': '1234', + 'content-digest': "sha-512=:vMVGexd7h7oBvi9aTwj05YvuCBTJaAYFPTwaxzu41/TyjXTueuKjxLlnTOhQfxE+YdA/QTiSXEkWh4gZ5zDZLg==:", + signature: "sig1=:Tk6ZvOqKxPysDpLPyjDRah76Uskr8OYxcuJasg4tSrD8qRaGBTji+WdMHxkkTqUX1cASaoqAdE3s7YDUFmlnCw==:", + 'signature-input': 'sig1=("@method" "@target-uri" "authorization" "content-digest" "content-length" "content-type");created=1670837620;keyid="keyid-97a3a431-8ee1-48fc-ac85-70e2f5eba8e5";alg="ed25519"', + authorization: 'GNAP 123454321' + }, + body: JSON.stringify(body) + }) +``` + +Verify signature + +```ts +const isValidSig = await validateSignature( + clientKey: jwk, + request: { + method: 'POST', + url: 'https://example.com', + headers: { + 'content-type': 'application/json', + 'content-length': '1234', + 'content-digest': "sha-512=:vMVGexd7h7oBvi9aTwj05YvuCBTJaAYFPTwaxzu41/TyjXTueuKjxLlnTOhQfxE+YdA/QTiSXEkWh4gZ5zDZLg==:", + signature: "sig1=:Tk6ZvOqKxPysDpLPyjDRah76Uskr8OYxcuJasg4tSrD8qRaGBTji+WdMHxkkTqUX1cASaoqAdE3s7YDUFmlnCw==:", + 'signature-input': 'sig1=("@method" "@target-uri" "authorization" "content-digest" "content-length" "content-type");created=1670837620;keyid="keyid-97a3a431-8ee1-48fc-ac85-70e2f5eba8e5";alg="ed25519"', + authorization: 'GNAP 123454321' + }, + body: JSON.stringify(body) + } +): +``` + +## Running the Postman signature app + +### Prerequisites + +- [Docker](https://docs.docker.com/engine/install/) configured to [run as non-root user](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user) + +### Docker build + +In order to build the docker container to run the signature app, run the following command. + +```shell +# from the root +docker build -f packages/http-signature-utils/Dockerfile -t rafiki-signatures . +``` + +The following environment variables can be set. + +| Name | Description | Note | +| -------- | -------------------------- | ----------------------------------------------------- | +| KEY_FILE | `/PATH/TO/private-key.pem` | Key file needs to be copied into the docker container | diff --git a/packages/http-signature-utils/jest.config.js b/packages/http-signature-utils/jest.config.js new file mode 100644 index 0000000000..a2667d704f --- /dev/null +++ b/packages/http-signature-utils/jest.config.js @@ -0,0 +1,17 @@ +'use strict' +// eslint-disable-next-line @typescript-eslint/no-var-requires +const baseConfig = require('../../jest.config.base.js') +// eslint-disable-next-line @typescript-eslint/no-var-requires +const packageName = require('./package.json').name + +module.exports = { + ...baseConfig, + clearMocks: true, + roots: [`/packages/${packageName}`], + testRegex: `(packages/${packageName}/.*/__tests__/.*|\\.(test|spec))\\.tsx?$`, + moduleDirectories: [`node_modules`, `packages/${packageName}/node_modules`], + modulePaths: [`/packages/${packageName}/src/`], + id: packageName, + displayName: packageName, + rootDir: '../..' +} diff --git a/packages/http-signature-utils/package.json b/packages/http-signature-utils/package.json new file mode 100644 index 0000000000..5f1e84f92d --- /dev/null +++ b/packages/http-signature-utils/package.json @@ -0,0 +1,34 @@ +{ + "name": "http-signature-utils", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "tsc --build tsconfig.json", + "test": "jest --passWithNoTests --maxWorkers=50%" + }, + "dependencies": { + "http-message-signatures": "^0.1.2", + "httpbis-digest-headers": "github:interledger/httpbis-digest-headers", + "jose": "^4.9.0", + "koa": "^2.13.4", + "koa-bodyparser": "^4.3.0", + "koa-json": "^2.0.2", + "koa-logger": "^3.2.1", + "koa-router": "^12.0.0", + "uuid": "^8.3.2" + }, + "devDependencies": { + "@types/koa": "2.13.5", + "@types/koa-bodyparser": "^4.3.7", + "@types/koa-json": "^2.0.20", + "@types/koa-logger": "^3.1.2", + "@types/koa-router": "^7.4.4", + "@types/node": "^18.7.12", + "@types/uuid": "^8.3.4", + "node-mocks-http": "^1.12.1", + "typescript": "^4.9.3" + } +} diff --git a/packages/http-signature-utils/postman-scripts/preRequestSignatures.js b/packages/http-signature-utils/postman-scripts/preRequestSignatures.js new file mode 100644 index 0000000000..555decdef5 --- /dev/null +++ b/packages/http-signature-utils/postman-scripts/preRequestSignatures.js @@ -0,0 +1,45 @@ +const requestUrl = request.url + .replace(/{{([A-Za-z]\w+)}}/g, (_, key) => pm.collectionVariables.get(key)) + .replace(/localhost:([3,4])000/g, (_, key) => + key === '3' ? 'backend' : 'peer-backend' + ) +const requestBody = + request.method === 'POST' + ? request.data.replace(/{{([A-Za-z]\w+)}}/g, (_, key) => + pm.collectionVariables.get(key) + ) + : undefined +const requestHeaders = JSON.parse( + JSON.stringify(request.headers).replace(/{{([A-Za-z]\w+)}}/g, (_, key) => + pm.collectionVariables.get(key) + ) +) + +// Request Signature Headers +pm.sendRequest( + { + url: pm.collectionVariables.get('signatureUrl'), + method: 'POST', + header: { + 'content-type': 'application/json' + }, + body: { + mode: 'raw', + raw: JSON.stringify({ + keyId: pm.collectionVariables.get('keyId'), + request: { + url: requestUrl, + method: request.method, + headers: requestHeaders, + body: requestBody + } + }) + } + }, + (_, res) => { + const headers = res.json() + for (let [key, value] of Object.entries(headers)) { + pm.request.headers.add({ key, value }) + } + } +) diff --git a/packages/http-signature-utils/postman-scripts/preRequestSignaturesGrantRequest.js b/packages/http-signature-utils/postman-scripts/preRequestSignaturesGrantRequest.js new file mode 100644 index 0000000000..b241b70e4b --- /dev/null +++ b/packages/http-signature-utils/postman-scripts/preRequestSignaturesGrantRequest.js @@ -0,0 +1,69 @@ +const url = require('url') + +const body = JSON.parse(request.data) +const client = url.parse(body.client) +const jwkUrl = `http://localhost:${client.host === 'backend' ? '3' : '4'}000${ + client.path +}/jwks.json` +pm.collectionVariables.set( + 'signatureUrl', + pm.collectionVariables.get( + client.host === 'backend' ? 'SignatureHost' : 'PeerSignatureHost' + ) +) + +const requestUrl = request.url.replace(/{{([A-Za-z]\w+)}}/g, (_, key) => + pm.collectionVariables.get(key) +) +const requestBody = request.data.replace(/{{([A-Za-z]\w+)}}/g, (_, key) => + pm.collectionVariables.get(key) +) +const requestHeaders = JSON.parse( + JSON.stringify(request.headers).replace(/{{([A-Za-z]\w+)}}/g, (_, key) => + pm.collectionVariables.get(key) + ) +) + +// Request Client JWK +pm.sendRequest( + { + url: jwkUrl, + method: 'GET', + header: { + Host: client.host + } + }, + (err, res) => { + const keys = res.json() + pm.collectionVariables.set('keyId', keys.keys[0].kid) + + // Request Signature Headers + pm.sendRequest( + { + url: pm.collectionVariables.get('signatureUrl'), + method: 'POST', + header: { + 'content-type': 'application/json' + }, + body: { + mode: 'raw', + raw: JSON.stringify({ + keyId: pm.collectionVariables.get('keyId'), + request: { + url: requestUrl, + method: request.method, + headers: requestHeaders, + body: requestBody + } + }) + } + }, + (_, res) => { + const headers = res.json() + for (let [key, value] of Object.entries(headers)) { + pm.request.headers.add({ key, value }) + } + } + ) + } +) diff --git a/packages/http-signature-utils/src/app.ts b/packages/http-signature-utils/src/app.ts new file mode 100644 index 0000000000..01a01ac305 --- /dev/null +++ b/packages/http-signature-utils/src/app.ts @@ -0,0 +1,40 @@ +import Koa from 'koa' +import Router from 'koa-router' + +import logger from 'koa-logger' +import json from 'koa-json' +import bodyParser from 'koa-bodyparser' + +import { parseOrProvisionKey } from './utils/key' +import { createHeaders } from './utils/headers' + +const app = new Koa() +const router = new Router() + +// Load key +const privateKey = parseOrProvisionKey(process.env.KEY_FILE) + +// Router +router.post('/', async (ctx): Promise => { + const { request, keyId } = ctx.request.body + if (!keyId || !request.headers || !request.method || !request.url) { + ctx.status = 400 + return + } + const headers = await createHeaders({ request, privateKey, keyId }) + delete headers['Content-Length'] + delete headers['Content-Type'] + ctx.body = headers +}) + +// Middlewares +app.use(json()) +app.use(logger()) +app.use(bodyParser()) + +// Routes +app.use(router.routes()).use(router.allowedMethods()) + +app.listen(3000, () => { + console.log('HTTP Signature Manager started.') +}) diff --git a/packages/http-signature-utils/src/index.ts b/packages/http-signature-utils/src/index.ts new file mode 100644 index 0000000000..d326119e75 --- /dev/null +++ b/packages/http-signature-utils/src/index.ts @@ -0,0 +1,7 @@ +export { createHeaders, Headers } from './utils/headers' +export { generateJwk, JWK } from './utils/jwk' +export { parseOrProvisionKey } from './utils/key' +export { createSignatureHeaders } from './utils/signatures' +export { validateSignatureHeaders, validateSignature } from './utils/validation' +export { generateTestKeys, TestKeys } from './test-utils/keys' +export { RequestLike } from 'http-message-signatures' diff --git a/packages/http-signature-utils/src/test-utils/keys.ts b/packages/http-signature-utils/src/test-utils/keys.ts new file mode 100644 index 0000000000..e821ae9bed --- /dev/null +++ b/packages/http-signature-utils/src/test-utils/keys.ts @@ -0,0 +1,14 @@ +import crypto from 'crypto' +import { v4 } from 'uuid' + +export type TestKeys = { + keyId: string + publicKey: crypto.KeyObject + privateKey: crypto.KeyObject +} + +export async function generateTestKeys(): Promise { + const keyId = v4() + const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519') + return { keyId, privateKey, publicKey } +} diff --git a/packages/http-signature-utils/src/utils/headers.ts b/packages/http-signature-utils/src/utils/headers.ts new file mode 100644 index 0000000000..d5f12bd852 --- /dev/null +++ b/packages/http-signature-utils/src/utils/headers.ts @@ -0,0 +1,50 @@ +import { createContentDigestHeader } from 'httpbis-digest-headers' +import { + createSignatureHeaders, + SignatureHeaders, + SignOptions +} from './signatures' + +interface ContentHeaders { + 'Content-Digest': string + 'Content-Length': string + 'Content-Type': string +} + +export interface Headers extends SignatureHeaders, Partial {} + +const createContentHeaders = (body: string): ContentHeaders => { + return { + 'Content-Digest': createContentDigestHeader( + JSON.stringify(JSON.parse(body)), + ['sha-512'] + ), + 'Content-Length': Buffer.from(body as string, 'utf-8').length.toString(), + 'Content-Type': 'application/json' + } +} + +export const createHeaders = async ({ + request, + privateKey, + keyId +}: SignOptions): Promise => { + let contentHeaders: ContentHeaders + if (request.body) { + contentHeaders = createContentHeaders(request.body as string) + request.headers = { ...request.headers, ...contentHeaders } + } + const signatureHeaders = await createSignatureHeaders({ + request, + privateKey, + keyId + }) + if (contentHeaders) { + return { + ...contentHeaders, + ...signatureHeaders + } + } else { + return signatureHeaders + } +} diff --git a/packages/open-payments/src/jwk.test.ts b/packages/http-signature-utils/src/utils/jwk.test.ts similarity index 100% rename from packages/open-payments/src/jwk.test.ts rename to packages/http-signature-utils/src/utils/jwk.test.ts diff --git a/packages/open-payments/src/jwk.ts b/packages/http-signature-utils/src/utils/jwk.ts similarity index 76% rename from packages/open-payments/src/jwk.ts rename to packages/http-signature-utils/src/utils/jwk.ts index 19fc51e954..2a7f81b72e 100644 --- a/packages/open-payments/src/jwk.ts +++ b/packages/http-signature-utils/src/utils/jwk.ts @@ -1,5 +1,17 @@ import { createPublicKey, generateKeyPairSync, KeyObject } from 'crypto' -import { JWK } from './types' +import { JWK as JoseWk } from 'jose' + +export interface JWK extends JoseWk { + kid: string + alg: string + kty: string + crv: string + x: string + use?: string + exp?: number + nbf?: number + revoked?: boolean +} export const generateJwk = ({ privateKey: providedPrivateKey, diff --git a/packages/backend/src/config/app.test.ts b/packages/http-signature-utils/src/utils/key.test.ts similarity index 98% rename from packages/backend/src/config/app.test.ts rename to packages/http-signature-utils/src/utils/key.test.ts index 2e3ceee129..06212e5433 100644 --- a/packages/backend/src/config/app.test.ts +++ b/packages/http-signature-utils/src/utils/key.test.ts @@ -1,7 +1,7 @@ import * as assert from 'assert' import * as crypto from 'crypto' import * as fs from 'fs' -import { parseOrProvisionKey } from './app' +import { parseOrProvisionKey } from './key' describe('Config', (): void => { describe('parseOrProvisionKey', (): void => { diff --git a/packages/http-signature-utils/src/utils/key.ts b/packages/http-signature-utils/src/utils/key.ts new file mode 100644 index 0000000000..e17c878325 --- /dev/null +++ b/packages/http-signature-utils/src/utils/key.ts @@ -0,0 +1,32 @@ +import * as crypto from 'crypto' +import * as fs from 'fs' + +export function parseOrProvisionKey( + keyFile: string | undefined +): crypto.KeyObject { + const TMP_DIR = './tmp' + if (keyFile) { + try { + const key = crypto.createPrivateKey(fs.readFileSync(keyFile)) + const jwk = key.export({ format: 'jwk' }) + if (jwk.crv === 'Ed25519') { + console.log(`Key ${keyFile} loaded.`) + return key + } else { + console.log('Private key is not EdDSA-Ed25519 key. Generating new key.') + } + } catch (err) { + console.log('Private key could not be loaded.') + throw err + } + } + const keypair = crypto.generateKeyPairSync('ed25519') + if (!fs.existsSync(TMP_DIR)) { + fs.mkdirSync(TMP_DIR) + } + fs.writeFileSync( + `${TMP_DIR}/private-key-${new Date().getTime()}.pem`, + keypair.privateKey.export({ format: 'pem', type: 'pkcs8' }) + ) + return keypair.privateKey +} diff --git a/packages/open-payments/src/client/signatures.ts b/packages/http-signature-utils/src/utils/signatures.ts similarity index 88% rename from packages/open-payments/src/client/signatures.ts rename to packages/http-signature-utils/src/utils/signatures.ts index 8bc48bec93..95a4ee419d 100644 --- a/packages/open-payments/src/client/signatures.ts +++ b/packages/http-signature-utils/src/utils/signatures.ts @@ -6,13 +6,13 @@ import { Signer } from 'http-message-signatures' -interface SignOptions { +export interface SignOptions { request: RequestLike privateKey: KeyLike keyId: string } -interface SignatureHeaders { +export interface SignatureHeaders { Signature: string 'Signature-Input': string } @@ -29,7 +29,7 @@ export const createSignatureHeaders = async ({ keyId }: SignOptions): Promise => { const components = ['@method', '@target-uri'] - if (request.headers['Authorization']) { + if (request.headers['Authorization'] || request.headers['authorization']) { components.push('authorization') } if (request.body) { diff --git a/packages/http-signature-utils/src/utils/validation.test.ts b/packages/http-signature-utils/src/utils/validation.test.ts new file mode 100644 index 0000000000..d342170679 --- /dev/null +++ b/packages/http-signature-utils/src/utils/validation.test.ts @@ -0,0 +1,91 @@ +import { validateSignatureHeaders, validateSignature } from './validation' +import { createHeaders } from './headers' +import { RequestLike } from 'http-message-signatures' +import { TestKeys, generateTestKeys } from '../test-utils/keys' +import { generateJwk, JWK } from './jwk' +import { createContentDigestHeader } from 'httpbis-digest-headers' + +describe('Signature Verification', (): void => { + let testKeys: TestKeys + let testClientKey: JWK + + beforeEach(async (): Promise => { + testKeys = await generateTestKeys() + testClientKey = generateJwk({ + privateKey: testKeys.privateKey, + keyId: testKeys.keyId + }) + }) + test.each` + title | withAuthorization | withRequestBody + ${''} | ${true} | ${true} + ${' without an authorization header'} | ${false} | ${true} + ${' without a request body'} | ${true} | ${false} + `( + 'can validate signature headers and signature', + async ({ withAuthorization, withRequestBody }): Promise => { + const testRequestBody = JSON.stringify({ foo: 'bar' }) + + const headers = {} + if (withAuthorization) { + headers['authorization'] = 'GNAP test-access-token' + } + + const request: RequestLike = { + headers, + method: 'GET', + url: 'http://example.com/test' + } + if (withRequestBody) { + request.body = testRequestBody + } + + const contentAndSigHeaders = await createHeaders({ + request, + privateKey: testKeys.privateKey, + keyId: testKeys.keyId + }) + const lowerHeaders = Object.fromEntries( + Object.entries(contentAndSigHeaders).map(([k, v]) => [ + k.toLowerCase(), + v + ]) + ) + request.headers = { ...request.headers, ...lowerHeaders } + + expect(validateSignatureHeaders(request)).toEqual(true) + await expect(validateSignature(testClientKey, request)).resolves.toEqual( + true + ) + } + ) + + test.each` + title | sigInputHeader + ${'fails if a component is not in lower case'} | ${'sig1=("@METHOD" "@target-uri" "content-digest" "content-length" "content-type" "authorization");created=1618884473;keyid="gnap-key"'} + ${'fails @method is missing'} | ${'sig1=("@target-uri" "content-digest" "content-length" "content-type");created=1618884473;keyid="gnap-key"'} + ${'fails if @target-uri is missing'} | ${'sig1=("@method" "content-digest" "content-length" "content-type");created=1618884473;keyid="gnap-key"'} + ${'fails if @content-digest is missing while body is present'} | ${'sig1=("@method" "@target-uri" "content-length" "content-type");created=1618884473;keyid="gnap-key"'} + ${'fails if authorization header is present in headers but not in signature input'} | ${'sig1=("@method" "@target-uri" "content-digest" "content-length" "content-type");created=1618884473;keyid="gnap-key"'} + `( + 'validates signature header and $title', + async ({ sigInputHeader }): Promise => { + const testRequestBody = JSON.stringify({ foo: 'bar' }) + const request = { + headers: { + 'content-type': 'application/json', + 'content-digest': createContentDigestHeader(testRequestBody, [ + 'sha-512' + ]), + 'content-length': '1234', + 'signature-input': sigInputHeader, + authorization: 'GNAP test-access-token' + }, + method: 'GET', + url: 'http://example.com/test', + body: testRequestBody + } + expect(validateSignatureHeaders(request)).toBe(false) + } + ) +}) diff --git a/packages/http-signature-utils/src/utils/validation.ts b/packages/http-signature-utils/src/utils/validation.ts new file mode 100644 index 0000000000..964ea24922 --- /dev/null +++ b/packages/http-signature-utils/src/utils/validation.ts @@ -0,0 +1,116 @@ +import * as crypto from 'crypto' +import { RequestLike } from 'http-message-signatures' +import { verifyContentDigest } from 'httpbis-digest-headers' +import { importJWK } from 'jose' +import { JWK } from './jwk' + +export function validateSignatureHeaders(request: RequestLike): boolean { + const sig = request.headers['signature'] + const sigInput = request.headers['signature-input'] as string + + const sigInputComponents = getSigInputComponents(sigInput ?? '') + if ( + !sigInputComponents || + !validateSigInputComponents(sigInputComponents, request) + ) + return false + + return ( + sig && sigInput && typeof sig === 'string' && typeof sigInput === 'string' + ) +} + +export async function validateSignature( + clientKey: JWK, + request: RequestLike +): Promise { + const sig = request.headers['signature'] as string + const sigInput = request.headers['signature-input'] as string + const challenge = sigInputToChallenge(sigInput, request) + if (!challenge) { + return false + } + + const publicKey = (await importJWK(clientKey)) as crypto.KeyLike + const data = Buffer.from(challenge) + return crypto.verify( + null, + data, + publicKey, + Buffer.from(sig.replace('sig1=', ''), 'base64') + ) +} + +function sigInputToChallenge( + sigInput: string, + request: RequestLike +): string | null { + const sigInputComponents = getSigInputComponents(sigInput) + + if ( + !sigInputComponents || + !validateSigInputComponents(sigInputComponents, request) + ) + return null + + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures-09#section-2.3 + let signatureBase = '' + for (const component of sigInputComponents) { + if (component === '@method') { + signatureBase += `"@method": ${request.method}\n` + } else if (component === '@target-uri') { + signatureBase += `"@target-uri": ${request.url}\n` + } else { + signatureBase += `"${component}": ${request.headers[component]}\n` + } + } + + signatureBase += `"@signature-params": ${( + request.headers['signature-input'] as string + )?.replace('sig1=', '')}` + return signatureBase +} + +function getSigInputComponents(sigInput: string): string[] | null { + // https://datatracker.ietf.org/doc/html/rfc8941#section-4.1.1.1 + const messageComponents = sigInput + .split('sig1=')[1] + ?.split(';')[0] + ?.split(' ') + return messageComponents + ? messageComponents.map((component) => component.replace(/[()"]/g, '')) + : null +} + +function validateSigInputComponents( + sigInputComponents: string[], + request: RequestLike +): boolean { + // https://datatracker.ietf.org/doc/html/draft-ietf-gnap-core-protocol#section-7.3.1 + + for (const component of sigInputComponents) { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures-09#section-2.1 + if (component !== component.toLowerCase()) return false + } + + const isValidContentDigest = + !sigInputComponents.includes('content-digest') || + (!!request.headers['content-digest'] && + !!request.headers['content-length'] && + !!request.headers['content-type'] && + request.body && + Object.keys(request.body).length > 0 && + sigInputComponents.includes('content-digest') && + verifyContentDigest( + request.body, + request.headers['content-digest'] as string + )) + + return !( + !isValidContentDigest || + !sigInputComponents.includes('@method') || + !sigInputComponents.includes('@target-uri') || + (request.headers['authorization'] && + !sigInputComponents.includes('authorization')) + ) +} diff --git a/packages/http-signature-utils/tsconfig.json b/packages/http-signature-utils/tsconfig.json new file mode 100644 index 0000000000..bc4252bfc2 --- /dev/null +++ b/packages/http-signature-utils/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "declaration": true + }, + "include": ["src/**/*"], + "exclude": ["**/*.test.ts", "src/test/*"] +} diff --git a/packages/mock-account-provider/app/lib/apolloClient.ts b/packages/mock-account-provider/app/lib/apolloClient.ts index 2f83ff990f..9df955c8fb 100644 --- a/packages/mock-account-provider/app/lib/apolloClient.ts +++ b/packages/mock-account-provider/app/lib/apolloClient.ts @@ -10,7 +10,7 @@ import { CONFIG } from './parse_config' import { onError } from '@apollo/client/link/error' const httpLink = createHttpLink({ - uri: CONFIG.self.graphqlUrl + uri: CONFIG.seed.self.graphqlUrl }) const errorLink = onError(({ graphQLErrors }) => { diff --git a/packages/mock-account-provider/app/lib/crypto.server.ts b/packages/mock-account-provider/app/lib/crypto.server.ts deleted file mode 100644 index 3c9d7c8cbc..0000000000 --- a/packages/mock-account-provider/app/lib/crypto.server.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createPublicKey, generateKeyPairSync, type JsonWebKey } from 'crypto' - -type OpenPaymentsJWK = JsonWebKey & { - alg: 'EdDSA' - kid: string -} - -export const generateJwk = ({ keyId }: { keyId: string }): OpenPaymentsJWK => { - const jwk = createPublicKey(generateKeyPairSync('ed25519').privateKey).export( - { - format: 'jwk' - } - ) - - return { - alg: 'EdDSA', - kid: keyId, - kty: jwk.kty, - crv: jwk.crv, - x: jwk.x - } -} diff --git a/packages/mock-account-provider/app/lib/parse_config.ts b/packages/mock-account-provider/app/lib/parse_config.ts index 2af4e39896..885fd87fbd 100644 --- a/packages/mock-account-provider/app/lib/parse_config.ts +++ b/packages/mock-account-provider/app/lib/parse_config.ts @@ -1,5 +1,7 @@ +import type * as crypto from 'crypto' import { parse } from 'yaml' import { readFileSync } from 'fs' +import { parseOrProvisionKey } from 'http-signature-utils' export interface Self { graphqlUrl: string @@ -40,8 +42,16 @@ export interface SeedInstance { fees: Array } -export const CONFIG: SeedInstance = parse( - readFileSync(process.env.SEED_FILE_LOCATION || `./seed.example.yml`).toString( - 'utf8' - ) -) +export interface Config { + seed: SeedInstance + key: crypto.KeyObject +} + +export const CONFIG: Config = { + seed: parse( + readFileSync( + process.env.SEED_FILE_LOCATION || `./seed.example.yml` + ).toString('utf8') + ), + key: parseOrProvisionKey(process.env.KEY_FILE) +} diff --git a/packages/mock-account-provider/app/lib/run_seed.ts b/packages/mock-account-provider/app/lib/run_seed.ts index 03d9d6a30c..bbceda3ffd 100644 --- a/packages/mock-account-provider/app/lib/run_seed.ts +++ b/packages/mock-account-provider/app/lib/run_seed.ts @@ -1,6 +1,5 @@ import * as _ from 'lodash' -import { CONFIG } from './parse_config' -import type { SeedInstance, Account, Peering } from './parse_config' +import { CONFIG, type Config, type Account, type Peering } from './parse_config' import { createPeer, addPeerLiquidity, @@ -9,11 +8,11 @@ import { } from './requesters' import { v4 } from 'uuid' import { mockAccounts } from './accounts.server' -import { generateJwk } from './crypto.server' +import { generateJwk } from 'http-signature-utils' -export async function setupFromSeed(config: SeedInstance): Promise { +export async function setupFromSeed(config: Config): Promise { const peerResponses = await Promise.all( - _.map(config.peers, async (peer: Peering) => { + _.map(config.seed.peers, async (peer: Peering) => { const peerResponse = await createPeer( peer.peerIlpAddress, peer.peerUrl, @@ -25,7 +24,7 @@ export async function setupFromSeed(config: SeedInstance): Promise { } const transferUid = v4() const liquidity = await addPeerLiquidity( - config.self.graphqlUrl, + config.seed.self.graphqlUrl, peerResponse.id, peer.initialLiquidity, transferUid @@ -40,7 +39,7 @@ export async function setupFromSeed(config: SeedInstance): Promise { await mockAccounts.clearAccounts() const accountResponses = await Promise.all( - _.map(config.accounts, async (account: Account) => { + _.map(config.seed.accounts, async (account: Account) => { await mockAccounts.create( account.id, account.name, @@ -55,9 +54,9 @@ export async function setupFromSeed(config: SeedInstance): Promise { ) } const paymentPointer = await createPaymentPointer( - config.self.graphqlUrl, + config.seed.self.graphqlUrl, account.name, - `https://${CONFIG.self.hostname}/${account.path}`, + `https://${CONFIG.seed.self.hostname}/${account.path}`, account.asset, account.scale ) @@ -70,15 +69,17 @@ export async function setupFromSeed(config: SeedInstance): Promise { await createPaymentPointerKey({ paymentPointerId: paymentPointer.id, - jwk: JSON.stringify(generateJwk({ keyId: `keyid-${account.id}` })) + jwk: JSON.stringify( + generateJwk({ keyId: `keyid-${account.id}`, privateKey: config.key }) + ) }) return paymentPointer }) ) console.log(JSON.stringify(accountResponses, null, 2)) - const envVarStrings = _.map(config.accounts, (account) => { - return `${account.postmanEnvVar}: http://localhost:${CONFIG.self.openPaymentPublishedPort}/${account.path} hostname: ${CONFIG.self.hostname}` + const envVarStrings = _.map(config.seed.accounts, (account) => { + return `${account.postmanEnvVar}: http://localhost:${CONFIG.seed.self.openPaymentPublishedPort}/${account.path} hostname: ${CONFIG.seed.self.hostname}` }) console.log(envVarStrings.join('\n')) } diff --git a/packages/mock-account-provider/app/routes/quotes.ts b/packages/mock-account-provider/app/routes/quotes.ts index d043f83e27..ee422e4ab3 100644 --- a/packages/mock-account-provider/app/routes/quotes.ts +++ b/packages/mock-account-provider/app/routes/quotes.ts @@ -32,7 +32,7 @@ export type Quote = { export async function action({ request }: ActionArgs) { const receivedQuote: Quote = await request.json() - const feeStructure = CONFIG.fees[0] + const feeStructure = CONFIG.seed.fees[0] if (receivedQuote.paymentType == PaymentType.FixedDelivery) { // TODO: handle quote fee calculation for different assets/scales if ( diff --git a/packages/mock-account-provider/package.json b/packages/mock-account-provider/package.json index 21c0e5dc13..842496656c 100644 --- a/packages/mock-account-provider/package.json +++ b/packages/mock-account-provider/package.json @@ -2,7 +2,8 @@ "name": "mock-account-provider", "sideEffects": false, "scripts": { - "build": "remix build", + "build:deps": "pnpm --filter http-signature-utils build", + "build": "pnpm build:deps && remix build", "dev": "PORT=3300 remix dev", "start": "remix-serve build" }, @@ -15,6 +16,7 @@ "@types/uuid": "^8.3.4", "axios": "^1.1.3", "graphql": "^16.6.0", + "http-signature-utils": "workspace:../http-signature-utils", "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/packages/open-payments/package.json b/packages/open-payments/package.json index b6f09d4226..2ce1d02119 100644 --- a/packages/open-payments/package.json +++ b/packages/open-payments/package.json @@ -7,7 +7,7 @@ "dist/**/*" ], "scripts": { - "build:deps": "pnpm --filter openapi build", + "build:deps": "pnpm --filter openapi build && pnpm --filter http-signature-utils build", "build": "pnpm build:deps && pnpm clean && tsc --build tsconfig.json", "clean": "rm -fr dist/", "generate:types": "npx ts-node scripts/generate-types.ts", @@ -26,7 +26,7 @@ "dependencies": { "axios": "^1.1.2", "http-message-signatures": "^0.1.2", - "httpbis-digest-headers": "github:interledger/httpbis-digest-headers", + "http-signature-utils": "workspace:../http-signature-utils", "openapi": "workspace:../openapi", "pino": "^8.4.2" } diff --git a/packages/open-payments/src/client/requests.ts b/packages/open-payments/src/client/requests.ts index 5d6afed962..9d0474fc19 100644 --- a/packages/open-payments/src/client/requests.ts +++ b/packages/open-payments/src/client/requests.ts @@ -1,9 +1,8 @@ import axios, { AxiosInstance } from 'axios' import { KeyLike } from 'crypto' -import { createContentDigestHeader } from 'httpbis-digest-headers' import { ResponseValidator } from 'openapi' import { BaseDeps } from '.' -import { createSignatureHeaders } from './signatures' +import { createHeaders } from 'http-signature-utils' interface GetArgs { url: string @@ -142,26 +141,26 @@ export const createAxiosInstance = (args: { if (args.privateKey && args.keyId) { axiosInstance.interceptors.request.use( async (config) => { - if (config.data) { - const data = JSON.stringify(config.data) - config.headers['Content-Digest'] = createContentDigestHeader(data, [ - 'sha-512' - ]) - config.headers['Content-Length'] = Buffer.from(data, 'utf-8').length - config.headers['Content-Type'] = 'application/json' - } - const sigHeaders = await createSignatureHeaders({ + const contentAndSigHeaders = await createHeaders({ request: { method: config.method.toUpperCase(), url: config.url, headers: config.headers, - body: config.data + body: config.data ? JSON.stringify(config.data) : undefined }, privateKey: args.privateKey, keyId: args.keyId }) - config.headers['Signature'] = sigHeaders['Signature'] - config.headers['Signature-Input'] = sigHeaders['Signature-Input'] + if (config.data) { + config.headers['Content-Digest'] = + contentAndSigHeaders['Content-Digest'] + config.headers['Content-Length'] = + contentAndSigHeaders['Content-Length'] + config.headers['Content-Type'] = contentAndSigHeaders['Content-Type'] + } + config.headers['Signature'] = contentAndSigHeaders['Signature'] + config.headers['Signature-Input'] = + contentAndSigHeaders['Signature-Input'] return config }, null, diff --git a/packages/open-payments/src/index.ts b/packages/open-payments/src/index.ts index 737cbe553f..6bf9194c15 100644 --- a/packages/open-payments/src/index.ts +++ b/packages/open-payments/src/index.ts @@ -19,5 +19,3 @@ export { AuthenticatedClient, UnauthenticatedClient } from './client' - -export { generateJwk } from './jwk' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72fec3406a..058505df42 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,6 +46,7 @@ importers: packages/auth: specifiers: '@adonisjs/fold': ^8.1.0 + '@faker-js/faker': ^7.4.0 '@koa/cors': ^3.3.0 '@koa/router': ^12.0.0 '@types/jest': ^28.1.8 @@ -57,9 +58,9 @@ importers: '@types/uuid': ^8.3.4 ajv: ^8.11.2 axios: ^0.27.2 + http-signature-utils: workspace:../http-signature-utils httpbis-digest-headers: github:interledger/httpbis-digest-headers jest-openapi: ^0.14.2 - jose: ^4.9.0 knex: ^0.95 koa: ^2.13.4 koa-bodyparser: ^4.3.0 @@ -83,8 +84,8 @@ importers: '@koa/router': 12.0.0 ajv: 8.11.2 axios: 0.27.2 + http-signature-utils: link:../http-signature-utils httpbis-digest-headers: github.com/interledger/httpbis-digest-headers/787b7af5ba1752337d696a7b1587193058173284 - jose: 4.9.0 knex: 0.95.15_pg@8.7.3 koa: 2.13.4 koa-bodyparser: 4.3.0 @@ -100,6 +101,7 @@ importers: testcontainers: 8.16.0 uuid: 8.3.2 devDependencies: + '@faker-js/faker': 7.4.0 '@types/jest': 28.1.8 '@types/koa': 2.13.5 '@types/koa-bodyparser': 4.3.7 @@ -146,7 +148,6 @@ importers: ajv: ^8.11.2 apollo-server: ^3.10.1 apollo-server-koa: ^3.10.1 - auth: workspace:../auth axios: 0.26.1 base64url: ^3.0.1 bcrypt: ^5.0.1 @@ -156,6 +157,7 @@ importers: graphql: ^16.6.0 graphql-scalars: ^1.18.0 graphql-tools: ^8.3.3 + http-signature-utils: workspace:../http-signature-utils ilp-packet: 3.1.4-alpha.1 ilp-protocol-ccp: ^1.2.2 ilp-protocol-ildcp: ^2.2.3 @@ -213,6 +215,7 @@ importers: graphql: 16.6.0 graphql-scalars: 1.18.0_graphql@16.6.0 graphql-tools: 8.3.3_onqnqwb3ubg5opvemcqf7c2qhy + http-signature-utils: link:../http-signature-utils ilp-packet: 3.1.4-alpha.1 ilp-protocol-ccp: 1.2.3 ilp-protocol-ildcp: 2.2.3 @@ -252,7 +255,6 @@ importers: '@types/tmp': 0.2.3 '@types/uuid': 8.3.4 apollo-server: 3.10.1_graphql@16.6.0 - auth: link:../auth cross-fetch: 3.1.5 ilp-protocol-stream: 2.7.1 jest-openapi: 0.14.2 @@ -268,6 +270,47 @@ importers: packages/frontend: specifiers: {} + packages/http-signature-utils: + specifiers: + '@types/koa': 2.13.5 + '@types/koa-bodyparser': ^4.3.7 + '@types/koa-json': ^2.0.20 + '@types/koa-logger': ^3.1.2 + '@types/koa-router': ^7.4.4 + '@types/node': ^18.7.12 + '@types/uuid': ^8.3.4 + http-message-signatures: ^0.1.2 + httpbis-digest-headers: github:interledger/httpbis-digest-headers + jose: ^4.9.0 + koa: ^2.13.4 + koa-bodyparser: ^4.3.0 + koa-json: ^2.0.2 + koa-logger: ^3.2.1 + koa-router: ^12.0.0 + node-mocks-http: ^1.12.1 + typescript: ^4.9.3 + uuid: ^8.3.2 + dependencies: + http-message-signatures: 0.1.2 + httpbis-digest-headers: github.com/interledger/httpbis-digest-headers/787b7af5ba1752337d696a7b1587193058173284 + jose: 4.9.0 + koa: 2.13.4 + koa-bodyparser: 4.3.0 + koa-json: 2.0.2 + koa-logger: 3.2.1 + koa-router: 12.0.0 + uuid: 8.3.2 + devDependencies: + '@types/koa': 2.13.5 + '@types/koa-bodyparser': 4.3.7 + '@types/koa-json': 2.0.20 + '@types/koa-logger': 3.1.2 + '@types/koa-router': 7.4.4 + '@types/node': 18.11.9 + '@types/uuid': 8.3.4 + node-mocks-http: 1.12.1 + typescript: 4.9.3 + packages/mock-account-provider: specifiers: '@apollo/client': ^3.6.9 @@ -284,6 +327,7 @@ importers: axios: ^1.1.3 eslint: ^8.20.0 graphql: ^16.6.0 + http-signature-utils: workspace:../http-signature-utils lodash: ^4.17.21 react: ^18.2.0 react-dom: ^18.2.0 @@ -299,6 +343,7 @@ importers: '@types/uuid': 8.3.4 axios: 1.1.3 graphql: 16.6.0 + http-signature-utils: link:../http-signature-utils lodash: 4.17.21 react: 18.2.0 react-dom: 18.2.0_react@18.2.0 @@ -319,7 +364,7 @@ importers: axios: ^1.1.2 base64url: ^3.0.1 http-message-signatures: ^0.1.2 - httpbis-digest-headers: github:interledger/httpbis-digest-headers + http-signature-utils: workspace:../http-signature-utils nock: ^13.2.9 openapi: workspace:../openapi openapi-typescript: ^4.5.0 @@ -330,7 +375,7 @@ importers: dependencies: axios: 1.1.2 http-message-signatures: 0.1.2 - httpbis-digest-headers: github.com/interledger/httpbis-digest-headers/787b7af5ba1752337d696a7b1587193058173284 + http-signature-utils: link:../http-signature-utils openapi: link:../openapi pino: 8.4.2 devDependencies: @@ -2355,7 +2400,6 @@ packages: /@faker-js/faker/7.4.0: resolution: {integrity: sha512-xDd3Tvkt2jgkx1LkuwwxpNBy/Oe+LkZBTwkgEFTiWpVSZgQ5sc/LenbHKRHbFl0dq/KFeeq/szyyPtpJRKY0fg==} engines: {node: '>=14.0.0', npm: '>=6.0.0'} - dev: false /@gar/promisify/1.1.3: resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} @@ -3824,7 +3868,7 @@ packages: /@types/accepts/1.3.5: resolution: {integrity: sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==} dependencies: - '@types/node': 18.7.6 + '@types/node': 18.11.9 /@types/acorn/4.0.6: resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} @@ -3880,7 +3924,7 @@ packages: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 18.7.13 + '@types/node': 18.11.9 /@types/bytes/3.1.1: resolution: {integrity: sha512-lOGyCnw+2JVPKU3wIV0srU0NyALwTBJlVSx5DfMQOFuuohA8y9S8orImpuIQikZ0uIQ8gehrRjxgQC1rLRi11w==} @@ -3902,7 +3946,7 @@ packages: /@types/connect/3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 18.7.13 + '@types/node': 18.11.9 /@types/content-disposition/0.5.5: resolution: {integrity: sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==} @@ -3920,7 +3964,7 @@ packages: '@types/connect': 3.4.35 '@types/express': 4.17.13 '@types/keygrip': 1.0.2 - '@types/node': 18.7.6 + '@types/node': 18.11.9 /@types/cors/2.8.12: resolution: {integrity: sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==} @@ -3962,7 +4006,7 @@ packages: /@types/express-serve-static-core/4.17.30: resolution: {integrity: sha512-gstzbTWro2/nFed1WXtf+TtrpwxH7Ggs4RLYTLbeVgIkUQOI3WG/JKjgeOU1zXDvezllupjrf8OPIdvTbIaVOQ==} dependencies: - '@types/node': 18.7.13 + '@types/node': 18.11.9 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 @@ -4073,6 +4117,24 @@ packages: dependencies: '@types/koa': 2.13.5 + /@types/koa-json/2.0.20: + resolution: {integrity: sha512-RuQ1Vlpsm/EC5wo2oWCgNnwneixnSQ9aHQAE7nwnbSiCibgfUO2wwSQN6rn8SfF97cRdN9hKAtoc9KSIuiGP6Q==} + dependencies: + '@types/koa': 2.13.5 + dev: true + + /@types/koa-logger/3.1.2: + resolution: {integrity: sha512-sioTA1xlKYiIgryANWPRHBkG3XGbWftw9slWADUPC+qvPIY/yRLSrhvX7zkJwMrntub5dPO0GuAoyGGf0yitfQ==} + dependencies: + '@types/koa': 2.13.5 + dev: true + + /@types/koa-router/7.4.4: + resolution: {integrity: sha512-3dHlZ6CkhgcWeF6wafEUvyyqjWYfKmev3vy1PtOmr0mBc3wpXPU5E8fBBd4YQo5bRpHPfmwC5yDaX7s4jhIN6A==} + dependencies: + '@types/koa': 2.13.5 + dev: true + /@types/koa-session/5.10.6: resolution: {integrity: sha512-p4rgkeRmiJu8XGC3eH2duRCNgnLUl6sjadEXH/AsieH/9fqYfXSZoZNC9CAe+FQK+QmM76hVyvuJ5Jrl5xxNeA==} dependencies: @@ -4102,7 +4164,7 @@ packages: '@types/http-errors': 1.8.2 '@types/keygrip': 1.0.2 '@types/koa-compose': 3.2.5 - '@types/node': 18.7.6 + '@types/node': 18.11.9 /@types/koa__cors/3.3.0: resolution: {integrity: sha512-FUN8YxcBakIs+walVe3+HcNP+Bxd0SB8BJHBWkglZ5C1XQWljlKcEFDG/dPiCIqwVCUbc5X0nYDlH62uEhdHMA==} @@ -4112,7 +4174,7 @@ packages: /@types/koa__router/8.0.11: resolution: {integrity: sha512-WXgKWpBsbS14kzmzD9LeFapOIa678h7zvUHxDwXwSx4ETKXhXLVUAToX6jZ/U7EihM7qwyD9W/BZvB0MRu7MTQ==} dependencies: - '@types/koa': 2.13.4 + '@types/koa': 2.13.5 dev: true /@types/lodash/4.14.184: @@ -4161,7 +4223,6 @@ packages: /@types/node/18.11.9: resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==} - dev: true /@types/node/18.7.12: resolution: {integrity: sha512-caqFX7GwvZ4KLnhpI9CfiMkgHKp6kvFAIgpkha0cjO7bAQvB6dWe+q3fTHmm7fQvv59pd4tPj77nriq2M6U2dw==} @@ -4236,7 +4297,7 @@ packages: resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==} dependencies: '@types/mime': 3.0.1 - '@types/node': 18.7.13 + '@types/node': 18.11.9 /@types/ssh2-streams/0.1.9: resolution: {integrity: sha512-I2J9jKqfmvXLR5GomDiCoHrEJ58hAOmFrekfFqmCFd+A6gaEStvWnPykoWUwld1PNg4G5ag1LwdA+Lz1doRJqg==} @@ -4628,7 +4689,6 @@ packages: engines: {node: '>=4'} dependencies: color-convert: 1.9.3 - dev: true /ansi-styles/4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} @@ -5560,7 +5620,6 @@ packages: ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 supports-color: 5.5.0 - dev: true /chalk/4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -5766,7 +5825,6 @@ packages: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: color-name: 1.1.3 - dev: true /color-convert/2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} @@ -5777,7 +5835,6 @@ packages: /color-name/1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true /color-name/1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -6730,7 +6787,6 @@ packages: /escape-string-regexp/1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - dev: true /escape-string-regexp/2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} @@ -7940,7 +7996,6 @@ packages: /has-flag/3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} - dev: true /has-flag/4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -8147,6 +8202,10 @@ packages: engines: {node: '>=10.17.0'} dev: true + /humanize-number/0.0.2: + resolution: {integrity: sha512-un3ZAcNQGI7RzaWGZzQDH47HETM4Wrj6z6E4TId8Yeq9w5ZKUVB1nrT2jwFheTUjEmqcgTjXDc959jum+ai1kQ==} + dev: false + /husky/8.0.2: resolution: {integrity: sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg==} engines: {node: '>=14'} @@ -9346,7 +9405,6 @@ packages: /json-stringify-safe/5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - dev: true /json-to-pretty-yaml/1.2.2: resolution: {integrity: sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==} @@ -9537,6 +9595,37 @@ packages: co: 4.6.0 koa-compose: 4.1.0 + /koa-is-json/1.0.0: + resolution: {integrity: sha512-+97CtHAlWDx0ndt0J8y3P12EWLwTLMXIfMnYDev3wOTwH/RpBGMlfn4bDXlMEg1u73K6XRE9BbUp+5ZAYoRYWw==} + dev: false + + /koa-json/2.0.2: + resolution: {integrity: sha512-8+dz0T2ekDuNN1svYoKPCV2txotQ3Ufg8Fn5bft1T48MPJWiC/HKmkk+3xj9EC/iNZuFYeLRazN2h2o3RSUXuQ==} + dependencies: + koa-is-json: 1.0.0 + streaming-json-stringify: 3.1.0 + dev: false + + /koa-logger/3.2.1: + resolution: {integrity: sha512-MjlznhLLKy9+kG8nAXKJLM0/ClsQp/Or2vI3a5rbSQmgl8IJBQO0KI5FA70BvW+hqjtxjp49SpH2E7okS6NmHg==} + engines: {node: '>= 7.6.0'} + dependencies: + bytes: 3.1.2 + chalk: 2.4.2 + humanize-number: 0.0.2 + passthrough-counter: 1.0.0 + dev: false + + /koa-router/12.0.0: + resolution: {integrity: sha512-zGrdiXygGYW8WvrzeGsHZvKnHs4DzyGoqJ9a8iHlRkiwuEAOAPyI27//OlhoWdgFAEIM3qbUgr0KCuRaP/TCag==} + engines: {node: '>= 12'} + dependencies: + http-errors: 2.0.0 + koa-compose: 4.1.0 + methods: 1.1.2 + path-to-regexp: 6.2.1 + dev: false + /koa-session/6.2.0: resolution: {integrity: sha512-l2ZC6D1BnRkIXhWkRgpewdqKn38/9/2WScmxyShuN408TxX+J/gUzdzGBIvGZaRwmezOU819sNpGmfFGLeDckg==} engines: {node: '>=7.6'} @@ -10594,6 +10683,22 @@ packages: range-parser: 1.2.1 type-is: 1.6.18 + /node-mocks-http/1.12.1: + resolution: {integrity: sha512-jrA7Sn3qI6GsHgWtUW3gMj0vO6Yz0nJjzg3jRZYjcfj4tzi8oWPauDK1qHVJoAxTbwuDHF1JiM9GISZ/ocI/ig==} + engines: {node: '>=0.6'} + dependencies: + accepts: 1.3.8 + content-disposition: 0.5.4 + depd: 1.1.2 + fresh: 0.5.2 + merge-descriptors: 1.0.1 + methods: 1.1.2 + mime: 1.6.0 + parseurl: 1.3.3 + range-parser: 1.2.1 + type-is: 1.6.18 + dev: true + /node-releases/2.0.6: resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==} dev: true @@ -11080,6 +11185,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /passthrough-counter/1.0.0: + resolution: {integrity: sha512-Wy8PXTLqPAN0oEgBrlnsXPMww3SYJ44tQ8aVrGAI4h4JZYCS0oYqsPqtPR8OhJpv6qFbpbB7XAn0liKV7EXubA==} + dev: false + /path-case/3.0.4: resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} dependencies: @@ -12380,6 +12489,13 @@ packages: /stream-slice/0.1.2: resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==} + /streaming-json-stringify/3.1.0: + resolution: {integrity: sha512-axtfs3BDxAsrZ9swD163FBrXZ8dhJJp6kUI6C97TvUZG9RHKfbg9nFbXqEheFNOb3IYMEt2ag9F62sWLFUZ4ug==} + dependencies: + json-stringify-safe: 5.0.1 + readable-stream: 2.3.7 + dev: false + /streamsearch/1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -12501,7 +12617,6 @@ packages: engines: {node: '>=4'} dependencies: has-flag: 3.0.0 - dev: true /supports-color/7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} @@ -13060,6 +13175,12 @@ packages: hasBin: true dev: true + /typescript/4.9.3: + resolution: {integrity: sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + /ua-parser-js/0.7.31: resolution: {integrity: sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==} dev: true diff --git a/tsconfig.json b/tsconfig.json index a816dfc8f2..42a52feee0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,8 @@ { "references": [ + { + "path": "packages/http-signature-utils" + }, { "path": "packages/openapi" }, From e9031457eeec69d483e3133ec647194bce9d14b5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Dec 2022 09:40:50 +0100 Subject: [PATCH 05/11] chore(deps): update nginx docker tag to v1.23.3 (#770) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- infrastructure/helm/tigerbeetle/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/helm/tigerbeetle/values.yaml b/infrastructure/helm/tigerbeetle/values.yaml index 3f6e3f6422..518390100c 100644 --- a/infrastructure/helm/tigerbeetle/values.yaml +++ b/infrastructure/helm/tigerbeetle/values.yaml @@ -32,7 +32,7 @@ statefulset: repository: nginx pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. - tag: "1.23.1" + tag: "1.23.3" replicas: 6 updateStrategy: From 4f36e6947930104eeb00af41d01f390c8a496358 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Dec 2022 09:57:04 +0100 Subject: [PATCH 06/11] chore(deps): update dependency @types/lodash to ^4.14.191 (#761) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/backend/package.json | 2 +- packages/mock-account-provider/package.json | 2 +- pnpm-lock.yaml | 16 ++++++---------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index bb13466d84..cafde90c04 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -21,7 +21,7 @@ "@types/koa-bodyparser": "^4.3.7", "@types/koa__cors": "^3.0.2", "@types/koa__router": "^8.0.11", - "@types/lodash": "^4.14.189", + "@types/lodash": "^4.14.191", "@types/luxon": "^3.0.0", "@types/react": "^18.0.25", "@types/rosie": "^0.0.40", diff --git a/packages/mock-account-provider/package.json b/packages/mock-account-provider/package.json index 842496656c..eac19dae97 100644 --- a/packages/mock-account-provider/package.json +++ b/packages/mock-account-provider/package.json @@ -26,7 +26,7 @@ "devDependencies": { "@remix-run/dev": "^1.6.8", "@remix-run/eslint-config": "^1.6.8", - "@types/lodash": "^4.14.184", + "@types/lodash": "^4.14.191", "@types/react": "^18.0.25", "@types/react-dom": "^18.0.9", "eslint": "^8.20.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 058505df42..4a8915a31c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,7 +138,7 @@ importers: '@types/koa-bodyparser': ^4.3.7 '@types/koa__cors': ^3.0.2 '@types/koa__router': ^8.0.11 - '@types/lodash': ^4.14.189 + '@types/lodash': ^4.14.191 '@types/luxon': ^3.0.0 '@types/react': ^18.0.25 '@types/rosie': ^0.0.40 @@ -248,7 +248,7 @@ importers: '@types/koa-bodyparser': 4.3.7 '@types/koa__cors': 3.3.0 '@types/koa__router': 8.0.11 - '@types/lodash': 4.14.189 + '@types/lodash': 4.14.191 '@types/luxon': 3.0.0 '@types/react': 18.0.25 '@types/rosie': 0.0.40 @@ -319,7 +319,7 @@ importers: '@remix-run/node': ^1.6.8 '@remix-run/react': ^1.6.8 '@remix-run/serve': ^1.6.8 - '@types/lodash': ^4.14.184 + '@types/lodash': ^4.14.191 '@types/node': ^18.7.12 '@types/react': ^18.0.25 '@types/react-dom': ^18.0.9 @@ -352,7 +352,7 @@ importers: devDependencies: '@remix-run/dev': 1.6.8_cgxlqhaetfbjollwaaytbbpdmu '@remix-run/eslint-config': 1.6.8_o3mauctgtumsl5j4kwehbgms54 - '@types/lodash': 4.14.184 + '@types/lodash': 4.14.191 '@types/react': 18.0.25 '@types/react-dom': 18.0.9 eslint: 8.22.0 @@ -4177,12 +4177,8 @@ packages: '@types/koa': 2.13.5 dev: true - /@types/lodash/4.14.184: - resolution: {integrity: sha512-RoZphVtHbxPZizt4IcILciSWiC6dcn+eZ8oX9IWEYfDMcocdd42f7NPI6fQj+6zI8y4E0L7gu2pcZKLGTRaV9Q==} - dev: true - - /@types/lodash/4.14.189: - resolution: {integrity: sha512-kb9/98N6X8gyME9Cf7YaqIMvYGnBSWqEci6tiettE6iJWH1XdJz/PO8LB0GtLCG7x8dU3KWhZT+lA1a35127tA==} + /@types/lodash/4.14.191: + resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==} dev: true /@types/long/4.0.1: From c499e2b39aafec307103bc6f24a89aab9b8cc3c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Dec 2022 10:13:57 +0100 Subject: [PATCH 07/11] chore(deps): update dependency koa to ^2.14.1 (#806) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/auth/package.json | 2 +- packages/backend/package.json | 2 +- packages/http-signature-utils/package.json | 2 +- packages/openapi/package.json | 2 +- pnpm-lock.yaml | 26 +++++++++++----------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/auth/package.json b/packages/auth/package.json index 152d6e11f6..385536ca49 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -24,7 +24,7 @@ "http-signature-utils": "workspace:../http-signature-utils", "httpbis-digest-headers": "github:interledger/httpbis-digest-headers", "knex": "^0.95", - "koa": "^2.13.4", + "koa": "^2.14.1", "koa-bodyparser": "^4.3.0", "koa-session": "^6.2.0", "node-mocks-http": "^1.11.0", diff --git a/packages/backend/package.json b/packages/backend/package.json index cafde90c04..bd7aa1e9b6 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -74,7 +74,7 @@ "iso8601-duration": "^2.1.1", "json-canonicalize": "^1.0.4", "knex": "^0.95", - "koa": "^2.13.1", + "koa": "^2.14.1", "koa-bodyparser": "^4.3.0", "lodash": "^4.17.21", "luxon": "^3.0.1", diff --git a/packages/http-signature-utils/package.json b/packages/http-signature-utils/package.json index 5f1e84f92d..5988b34cf3 100644 --- a/packages/http-signature-utils/package.json +++ b/packages/http-signature-utils/package.json @@ -13,7 +13,7 @@ "http-message-signatures": "^0.1.2", "httpbis-digest-headers": "github:interledger/httpbis-digest-headers", "jose": "^4.9.0", - "koa": "^2.13.4", + "koa": "^2.14.1", "koa-bodyparser": "^4.3.0", "koa-json": "^2.0.2", "koa-logger": "^3.2.1", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 40963acdb8..62314b2eda 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -25,7 +25,7 @@ "@types/jest": "^28.1.8", "@types/koa": "2.13.5", "@types/uuid": "^8.3.4", - "koa": "^2.13.4", + "koa": "^2.14.1", "node-mocks-http": "^1.11.0", "typescript": "^4.2.4", "uuid": "^8.3.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a8915a31c..213add52d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,7 +62,7 @@ importers: httpbis-digest-headers: github:interledger/httpbis-digest-headers jest-openapi: ^0.14.2 knex: ^0.95 - koa: ^2.13.4 + koa: ^2.14.1 koa-bodyparser: ^4.3.0 koa-session: ^6.2.0 nock: ^13.2.4 @@ -87,7 +87,7 @@ importers: http-signature-utils: link:../http-signature-utils httpbis-digest-headers: github.com/interledger/httpbis-digest-headers/787b7af5ba1752337d696a7b1587193058173284 knex: 0.95.15_pg@8.7.3 - koa: 2.13.4 + koa: 2.14.1 koa-bodyparser: 4.3.0 koa-session: 6.2.0 node-mocks-http: 1.11.0 @@ -167,7 +167,7 @@ importers: jest-openapi: ^0.14.2 json-canonicalize: ^1.0.4 knex: ^0.95 - koa: ^2.13.1 + koa: ^2.14.1 koa-bodyparser: ^4.3.0 lodash: ^4.17.21 luxon: ^3.0.1 @@ -206,7 +206,7 @@ importers: '@types/bcrypt': 5.0.0 add: 2.0.6 ajv: 8.11.2 - apollo-server-koa: 3.10.1_graphql@16.6.0+koa@2.13.4 + apollo-server-koa: 3.10.1_graphql@16.6.0+koa@2.14.1 axios: 0.26.1 base64url: 3.0.1 bcrypt: 5.0.1 @@ -223,7 +223,7 @@ importers: iso8601-duration: 2.1.1 json-canonicalize: 1.0.4 knex: 0.95.15_pg@8.7.3 - koa: 2.13.4 + koa: 2.14.1 koa-bodyparser: 4.3.0 lodash: 4.17.21 luxon: 3.0.1 @@ -282,7 +282,7 @@ importers: http-message-signatures: ^0.1.2 httpbis-digest-headers: github:interledger/httpbis-digest-headers jose: ^4.9.0 - koa: ^2.13.4 + koa: ^2.14.1 koa-bodyparser: ^4.3.0 koa-json: ^2.0.2 koa-logger: ^3.2.1 @@ -294,7 +294,7 @@ importers: http-message-signatures: 0.1.2 httpbis-digest-headers: github.com/interledger/httpbis-digest-headers/787b7af5ba1752337d696a7b1587193058173284 jose: 4.9.0 - koa: 2.13.4 + koa: 2.14.1 koa-bodyparser: 4.3.0 koa-json: 2.0.2 koa-logger: 3.2.1 @@ -395,7 +395,7 @@ importers: '@types/uuid': ^8.3.4 ajv: ^8.11.2 ajv-formats: ^2.1.1 - koa: ^2.13.4 + koa: ^2.14.1 node-mocks-http: ^1.11.0 openapi-default-setter: ^12.0.0 openapi-request-coercer: ^12.0.0 @@ -417,7 +417,7 @@ importers: '@types/jest': 28.1.8 '@types/koa': 2.13.5 '@types/uuid': 8.3.4 - koa: 2.13.4 + koa: 2.14.1 node-mocks-http: 1.11.0 typescript: 4.7.4 uuid: 8.3.2 @@ -4793,7 +4793,7 @@ packages: - supports-color dev: true - /apollo-server-koa/3.10.1_graphql@16.6.0+koa@2.13.4: + /apollo-server-koa/3.10.1_graphql@16.6.0+koa@2.14.1: resolution: {integrity: sha512-NdrmB6oIHca14L/xHwgFlnA/LnvlRfoVuHNzgB9o9gnQS4AeUDR8hzKJ2Zue45OEn0BeIBY1g5N/fX4oqE3WqA==} engines: {node: '>=12.0'} peerDependencies: @@ -4810,7 +4810,7 @@ packages: apollo-server-core: 3.10.1_graphql@16.6.0 apollo-server-types: 3.6.2_graphql@16.6.0 graphql: 16.6.0 - koa: 2.13.4 + koa: 2.14.1 koa-bodyparser: 4.3.0 koa-compose: 4.1.0 transitivePeerDependencies: @@ -9634,8 +9634,8 @@ packages: - supports-color dev: false - /koa/2.13.4: - resolution: {integrity: sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g==} + /koa/2.14.1: + resolution: {integrity: sha512-USJFyZgi2l0wDgqkfD27gL4YGno7TfUkcmOe6UOLFOVuN+J7FwnNu4Dydl4CUQzraM1lBAiGed0M9OVJoT0Kqw==} engines: {node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4} dependencies: accepts: 1.3.8 From cf3dacc29645b4d415b1b278a11df5c8fe87e627 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Dec 2022 10:17:27 +0100 Subject: [PATCH 08/11] fix(deps): update dependency ilp-protocol-ccp to ^1.2.3 (#776) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/backend/package.json | 2 +- pnpm-lock.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index bd7aa1e9b6..09e219f157 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -68,7 +68,7 @@ "graphql-tools": "^8.3.3", "http-signature-utils": "workspace:../http-signature-utils", "ilp-packet": "3.1.4-alpha.1", - "ilp-protocol-ccp": "^1.2.2", + "ilp-protocol-ccp": "^1.2.3", "ilp-protocol-ildcp": "^2.2.3", "ioredis": "^5.2.2", "iso8601-duration": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 213add52d2..9a22c6fa3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -159,7 +159,7 @@ importers: graphql-tools: ^8.3.3 http-signature-utils: workspace:../http-signature-utils ilp-packet: 3.1.4-alpha.1 - ilp-protocol-ccp: ^1.2.2 + ilp-protocol-ccp: ^1.2.3 ilp-protocol-ildcp: ^2.2.3 ilp-protocol-stream: ^2.7.1 ioredis: ^5.2.2 From dd6eed23b119574da9e8afd940fa63a50a1a6dce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Dec 2022 10:18:59 +0100 Subject: [PATCH 09/11] chore(deps): update dependency @swc/jest to ^0.2.24 (#837) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 104 ++++++++++++++++++++++++++++++++++--------------- 2 files changed, 73 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 9237946eba..8584a9baaa 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@commitlint/config-conventional": "^17.0.3", "@jest/types": "^28.1.3", "@swc/core": "^1.2.242", - "@swc/jest": "^0.2.23", + "@swc/jest": "^0.2.24", "@types/jest": "^28.1.8", "@typescript-eslint/eslint-plugin": "^5.34.0", "@typescript-eslint/parser": "^5.34.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a22c6fa3d..6d58295100 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,7 +8,7 @@ importers: '@commitlint/config-conventional': ^17.0.3 '@jest/types': ^28.1.3 '@swc/core': ^1.2.242 - '@swc/jest': ^0.2.23 + '@swc/jest': ^0.2.24 '@types/jest': ^28.1.8 '@typescript-eslint/eslint-plugin': ^5.34.0 '@typescript-eslint/parser': ^5.34.0 @@ -26,7 +26,7 @@ importers: '@commitlint/config-conventional': 17.0.3 '@jest/types': 28.1.3 '@swc/core': 1.2.242 - '@swc/jest': 0.2.23_@swc+core@1.2.242 + '@swc/jest': 0.2.24_@swc+core@1.2.242 '@types/jest': 28.1.8 '@typescript-eslint/eslint-plugin': 5.34.0_euudt5oqhhodkyae5tf6wjmsda '@typescript-eslint/parser': 5.34.0_4rv7y5c6xz3vfxwhbrcxxi73bq @@ -2953,7 +2953,7 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: '@jest/types': 28.1.3 - '@types/node': 18.7.13 + '@types/node': 18.11.9 chalk: 4.1.2 jest-message-util: 28.1.3 jest-util: 28.1.3 @@ -2974,14 +2974,14 @@ packages: '@jest/test-result': 28.1.3 '@jest/transform': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 18.7.13 + '@types/node': 18.11.9 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.3.2 exit: 0.1.2 graceful-fs: 4.2.10 jest-changed-files: 28.1.3 - jest-config: 28.1.3_@types+node@18.7.13 + jest-config: 28.1.3_@types+node@18.11.9 jest-haste-map: 28.1.3 jest-message-util: 28.1.3 jest-regex-util: 28.0.2 @@ -3016,7 +3016,7 @@ packages: dependencies: '@jest/fake-timers': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 18.7.13 + '@types/node': 18.11.9 jest-mock: 28.1.3 dev: true @@ -3043,7 +3043,7 @@ packages: dependencies: '@jest/types': 28.1.3 '@sinonjs/fake-timers': 9.1.2 - '@types/node': 18.7.13 + '@types/node': 18.11.9 jest-message-util: 28.1.3 jest-mock: 28.1.3 jest-util: 28.1.3 @@ -3075,7 +3075,7 @@ packages: '@jest/transform': 28.1.3 '@jest/types': 28.1.3 '@jridgewell/trace-mapping': 0.3.15 - '@types/node': 18.7.13 + '@types/node': 18.11.9 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -3174,7 +3174,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.7.13 + '@types/node': 18.11.9 '@types/yargs': 16.0.4 chalk: 4.1.2 dev: true @@ -3800,8 +3800,8 @@ packages: '@swc/core-win32-x64-msvc': 1.2.242 dev: true - /@swc/jest/0.2.23_@swc+core@1.2.242: - resolution: {integrity: sha512-ZLj17XjHbPtNsgqjm83qizENw05emLkKGu3WuPUttcy9hkngl0/kcc7fDbcSBpADS0GUtsO+iKPjZFWVAtJSlA==} + /@swc/jest/0.2.24_@swc+core@1.2.242: + resolution: {integrity: sha512-fwgxQbM1wXzyKzl1+IW0aGrRvAA8k0Y3NxFhKigbPjOJ4mCKnWEcNX9HQS3gshflcxq8YKhadabGUVfdwjCr6Q==} engines: {npm: '>= 7.0.0'} peerDependencies: '@swc/core': '*' @@ -3935,7 +3935,7 @@ packages: dependencies: '@types/http-cache-semantics': 4.0.1 '@types/keyv': 3.1.4 - '@types/node': 18.7.13 + '@types/node': 18.11.9 '@types/responselike': 1.0.0 dev: true @@ -3978,14 +3978,14 @@ packages: /@types/docker-modem/3.0.2: resolution: {integrity: sha512-qC7prjoEYR2QEe6SmCVfB1x3rfcQtUr1n4x89+3e0wSTMQ/KYCyf+/RAA9n2tllkkNc6//JMUZePdFRiGIWfaQ==} dependencies: - '@types/node': 18.7.13 + '@types/node': 18.11.9 '@types/ssh2': 1.11.5 /@types/dockerode/3.3.9: resolution: {integrity: sha512-SYRN5FF/qmwpxUT6snJP5D8k0wgoUKOGVs625XvpRJOOUi6s//UYI4F0tbyE3OmzpI70Fo1+aqpzX27zCrInww==} dependencies: '@types/docker-modem': 3.0.2 - '@types/node': 18.7.13 + '@types/node': 18.11.9 /@types/estree-jsx/0.0.1: resolution: {integrity: sha512-gcLAYiMfQklDCPjQegGn0TBAn9it05ISEsEhlKQUddIk7o2XDokOcTN7HBO8tznM0D9dGezvHEfRZBfZf6me0A==} @@ -4022,12 +4022,12 @@ packages: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 3.0.5 - '@types/node': 18.7.13 + '@types/node': 18.11.9 /@types/graceful-fs/4.1.5: resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} dependencies: - '@types/node': 18.7.13 + '@types/node': 18.11.9 dev: true /@types/hast/2.3.4: @@ -4095,7 +4095,7 @@ packages: /@types/jsonwebtoken/8.5.9: resolution: {integrity: sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==} dependencies: - '@types/node': 18.7.13 + '@types/node': 18.11.9 dev: true /@types/keygrip/1.0.2: @@ -4104,7 +4104,7 @@ packages: /@types/keyv/3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 18.7.13 + '@types/node': 18.11.9 dev: true /@types/koa-bodyparser/4.3.7: @@ -4226,6 +4226,7 @@ packages: /@types/node/18.7.13: resolution: {integrity: sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw==} + dev: true /@types/node/18.7.6: resolution: {integrity: sha512-EdxgKRXgYsNITy5mjjXjVE/CS8YENSdhiagGrLqjG0pvA2owgJ6i4l7wy/PFZGC0B1/H20lWKN7ONVDNYDZm7A==} @@ -4270,7 +4271,7 @@ packages: resolution: {integrity: sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==} dependencies: '@types/caseless': 0.12.2 - '@types/node': 18.7.13 + '@types/node': 18.11.9 '@types/tough-cookie': 4.0.2 form-data: 2.5.1 dev: true @@ -4278,7 +4279,7 @@ packages: /@types/responselike/1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: - '@types/node': 18.7.13 + '@types/node': 18.11.9 dev: true /@types/rosie/0.0.40: @@ -4298,18 +4299,18 @@ packages: /@types/ssh2-streams/0.1.9: resolution: {integrity: sha512-I2J9jKqfmvXLR5GomDiCoHrEJ58hAOmFrekfFqmCFd+A6gaEStvWnPykoWUwld1PNg4G5ag1LwdA+Lz1doRJqg==} dependencies: - '@types/node': 18.7.13 + '@types/node': 18.11.9 /@types/ssh2/0.5.52: resolution: {integrity: sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==} dependencies: - '@types/node': 18.7.13 + '@types/node': 18.11.9 '@types/ssh2-streams': 0.1.9 /@types/ssh2/1.11.5: resolution: {integrity: sha512-RaBsPKr+YP/slH8iR7XfC7chyomU+V57F/gJ5cMSP2n6/YWKVmeRLx7lrkgw4YYLpEW5lXLAdfZJqGo0PXboSA==} dependencies: - '@types/node': 18.7.13 + '@types/node': 18.11.9 /@types/stack-utils/2.0.1: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} @@ -4327,7 +4328,7 @@ packages: resolution: {integrity: sha512-mu/N4uvfDN2zVQQ5AYJI/g4qxn2bHB6521t1UuH09ShNWjebTqN0ZFuYK9uYjcgmI0dTQEs+Owi1EO6U0OkOZQ==} dependencies: '@types/cookiejar': 2.1.2 - '@types/node': 18.7.13 + '@types/node': 18.11.9 dev: true /@types/tmp/0.2.3: @@ -4348,7 +4349,7 @@ packages: /@types/ws/8.5.3: resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==} dependencies: - '@types/node': 18.7.13 + '@types/node': 18.11.9 dev: true /@types/yargs-parser/21.0.0: @@ -8874,7 +8875,7 @@ packages: '@jest/expect': 28.1.3 '@jest/test-result': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 18.7.13 + '@types/node': 18.11.9 chalk: 4.1.2 co: 4.6.0 dedent: 0.7.0 @@ -8921,6 +8922,45 @@ packages: - ts-node dev: true + /jest-config/28.1.3_@types+node@18.11.9: + resolution: {integrity: sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.18.10 + '@jest/test-sequencer': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 18.11.9 + babel-jest: 28.1.3_@babel+core@7.18.10 + chalk: 4.1.2 + ci-info: 3.3.2 + deepmerge: 4.2.2 + glob: 7.2.3 + graceful-fs: 4.2.10 + jest-circus: 28.1.3 + jest-environment-node: 28.1.3 + jest-get-type: 28.0.2 + jest-regex-util: 28.0.2 + jest-resolve: 28.1.3 + jest-runner: 28.1.3 + jest-util: 28.1.3 + jest-validate: 28.1.3 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 28.1.3 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + /jest-config/28.1.3_@types+node@18.7.13: resolution: {integrity: sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -9005,7 +9045,7 @@ packages: '@jest/environment': 28.1.3 '@jest/fake-timers': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 18.7.13 + '@types/node': 18.11.9 jest-mock: 28.1.3 jest-util: 28.1.3 dev: true @@ -9026,7 +9066,7 @@ packages: dependencies: '@jest/types': 28.1.3 '@types/graceful-fs': 4.1.5 - '@types/node': 18.7.13 + '@types/node': 18.11.9 anymatch: 3.1.2 fb-watchman: 2.0.1 graceful-fs: 4.2.10 @@ -9087,7 +9127,7 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: '@jest/types': 28.1.3 - '@types/node': 18.7.13 + '@types/node': 18.11.9 dev: true /jest-openapi/0.14.2: @@ -9150,7 +9190,7 @@ packages: '@jest/test-result': 28.1.3 '@jest/transform': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 18.7.13 + '@types/node': 18.11.9 chalk: 4.1.2 emittery: 0.10.2 graceful-fs: 4.2.10 @@ -9261,7 +9301,7 @@ packages: dependencies: '@jest/test-result': 28.1.3 '@jest/types': 28.1.3 - '@types/node': 18.7.13 + '@types/node': 18.11.9 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.10.2 @@ -9273,7 +9313,7 @@ packages: resolution: {integrity: sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} dependencies: - '@types/node': 18.7.13 + '@types/node': 18.11.9 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true From 5d0d8a4b0f0e15540c53c231d60f2709e1776cf1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Dec 2022 11:18:25 +0100 Subject: [PATCH 10/11] fix(deps): update dependency ioredis to ^5.2.4 (#778) * fix(deps): update dependency ioredis to ^5.2.4 * fix: outdated lock file Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Sabine Schaller --- packages/backend/package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 09e219f157..24e03e2ab6 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -70,7 +70,7 @@ "ilp-packet": "3.1.4-alpha.1", "ilp-protocol-ccp": "^1.2.3", "ilp-protocol-ildcp": "^2.2.3", - "ioredis": "^5.2.2", + "ioredis": "^5.2.4", "iso8601-duration": "^2.1.1", "json-canonicalize": "^1.0.4", "knex": "^0.95", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d58295100..05c4ec31b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,7 +162,7 @@ importers: ilp-protocol-ccp: ^1.2.3 ilp-protocol-ildcp: ^2.2.3 ilp-protocol-stream: ^2.7.1 - ioredis: ^5.2.2 + ioredis: ^5.2.4 iso8601-duration: ^2.1.1 jest-openapi: ^0.14.2 json-canonicalize: ^1.0.4 @@ -219,7 +219,7 @@ importers: ilp-packet: 3.1.4-alpha.1 ilp-protocol-ccp: 1.2.3 ilp-protocol-ildcp: 2.2.3 - ioredis: 5.2.2 + ioredis: 5.2.4 iso8601-duration: 2.1.1 json-canonicalize: 1.0.4 knex: 0.95.15_pg@8.7.3 @@ -8412,8 +8412,8 @@ packages: loose-envify: 1.4.0 dev: true - /ioredis/5.2.2: - resolution: {integrity: sha512-wryKc1ur8PcCmNwfcGkw5evouzpbDXxxkMkzPK8wl4xQfQf7lHe11Jotell5ikMVAtikXJEu/OJVaoV51BggRQ==} + /ioredis/5.2.4: + resolution: {integrity: sha512-qIpuAEt32lZJQ0XyrloCRdlEdUUNGG9i0UOk6zgzK6igyudNWqEBxfH6OlbnOOoBBvr1WB02mm8fR55CnikRng==} engines: {node: '>=12.22.0'} dependencies: '@ioredis/commands': 1.2.0 From b19919a68d1ff782193d171d83053579b5abdff2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Dec 2022 12:46:20 +0100 Subject: [PATCH 11/11] chore(deps): update dependency @types/react to ^18.0.26 (#853) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/backend/package.json | 2 +- packages/mock-account-provider/package.json | 2 +- pnpm-lock.yaml | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 24e03e2ab6..5f84ce13bc 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -23,7 +23,7 @@ "@types/koa__router": "^8.0.11", "@types/lodash": "^4.14.191", "@types/luxon": "^3.0.0", - "@types/react": "^18.0.25", + "@types/react": "^18.0.26", "@types/rosie": "^0.0.40", "@types/tmp": "^0.2.3", "@types/uuid": "^8.3.4", diff --git a/packages/mock-account-provider/package.json b/packages/mock-account-provider/package.json index eac19dae97..1b6ce922d6 100644 --- a/packages/mock-account-provider/package.json +++ b/packages/mock-account-provider/package.json @@ -27,7 +27,7 @@ "@remix-run/dev": "^1.6.8", "@remix-run/eslint-config": "^1.6.8", "@types/lodash": "^4.14.191", - "@types/react": "^18.0.25", + "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", "eslint": "^8.20.0", "typescript": "^4.7.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05c4ec31b5..45b00b4f16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -140,7 +140,7 @@ importers: '@types/koa__router': ^8.0.11 '@types/lodash': ^4.14.191 '@types/luxon': ^3.0.0 - '@types/react': ^18.0.25 + '@types/react': ^18.0.26 '@types/rosie': ^0.0.40 '@types/tmp': ^0.2.3 '@types/uuid': ^8.3.4 @@ -250,7 +250,7 @@ importers: '@types/koa__router': 8.0.11 '@types/lodash': 4.14.191 '@types/luxon': 3.0.0 - '@types/react': 18.0.25 + '@types/react': 18.0.26 '@types/rosie': 0.0.40 '@types/tmp': 0.2.3 '@types/uuid': 8.3.4 @@ -321,7 +321,7 @@ importers: '@remix-run/serve': ^1.6.8 '@types/lodash': ^4.14.191 '@types/node': ^18.7.12 - '@types/react': ^18.0.25 + '@types/react': ^18.0.26 '@types/react-dom': ^18.0.9 '@types/uuid': ^8.3.4 axios: ^1.1.3 @@ -353,7 +353,7 @@ importers: '@remix-run/dev': 1.6.8_cgxlqhaetfbjollwaaytbbpdmu '@remix-run/eslint-config': 1.6.8_o3mauctgtumsl5j4kwehbgms54 '@types/lodash': 4.14.191 - '@types/react': 18.0.25 + '@types/react': 18.0.26 '@types/react-dom': 18.0.9 eslint: 8.22.0 typescript: 4.7.4 @@ -4256,11 +4256,11 @@ packages: /@types/react-dom/18.0.9: resolution: {integrity: sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==} dependencies: - '@types/react': 18.0.25 + '@types/react': 18.0.26 dev: true - /@types/react/18.0.25: - resolution: {integrity: sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==} + /@types/react/18.0.26: + resolution: {integrity: sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==} dependencies: '@types/prop-types': 15.7.5 '@types/scheduler': 0.16.2