Skip to content

Commit 8f3cc39

Browse files
authored
chore: make signature verification & generation use ms for timestamp (#3236)
* chore(backend): make signature validation use milliseconds instead of seconds * chore(auth): make signature validation use milliseconds instead of seconds * chore(frontend): make signature generation use milliseconds for timestamp * chore(mase): make signature generation use milliseconds for timestamp * chore(bruno): make signature generation use milliseconds for timestamp * test(backend): update webhook service tests * test(backend): update test file to stop logging out warning * test(integration): sign apollo client requests * chore(ci): ignore trivy vulnerability indefinitely
1 parent 5139a2e commit 8f3cc39

File tree

24 files changed

+137
-62
lines changed

24 files changed

+137
-62
lines changed

.trivyignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
CVE-2024-21538 exp:2024-12-31
1+
CVE-2024-21538

bruno/collections/Rafiki/scripts.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ const scripts = {
8585
generateAuthApiSignature: function (body) {
8686
const version = bru.getEnvVar('authApiSignatureVersion')
8787
const secret = bru.getEnvVar('authApiSignatureSecret')
88-
const timestamp = Math.round(new Date().getTime() / 1000)
88+
const timestamp = Date.now()
8989
const payload = `${timestamp}.${canonicalize(body)}`
9090
const hmac = createHmac('sha256', secret)
9191
hmac.update(payload)
@@ -97,7 +97,7 @@ const scripts = {
9797
generateBackendApiSignature: function (body) {
9898
const version = bru.getEnvVar('backendApiSignatureVersion')
9999
const secret = bru.getEnvVar('backendApiSignatureSecret')
100-
const timestamp = Math.round(new Date().getTime() / 1000)
100+
const timestamp = Date.now()
101101
const payload = `${timestamp}.${canonicalize(body)}`
102102
const hmac = createHmac('sha256', secret)
103103
hmac.update(payload)

localenv/mock-account-servicing-entity/app/lib/apolloClient.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const errorLink = onError(({ graphQLErrors }) => {
5151
const authLink = setContext((request, { headers }) => {
5252
if (!process.env.SIGNATURE_SECRET || !process.env.SIGNATURE_VERSION)
5353
return { headers }
54-
const timestamp = Math.round(new Date().getTime() / 1000)
54+
const timestamp = Date.now()
5555
const version = process.env.SIGNATURE_VERSION
5656

5757
const { query, variables, operationName } = request

packages/auth/src/config/app.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const Config = {
5858
authServerUrl: envString('AUTH_SERVER_URL'),
5959
adminApiSecret: process.env.ADMIN_API_SECRET, // optional
6060
adminApiSignatureVersion: envInt('ADMIN_API_SIGNATURE_VERSION', 1),
61-
adminApiSignatureTtl: envInt('ADMIN_API_SIGNATURE_TTL_SECONDS', 30),
61+
adminApiSignatureTtlSeconds: envInt('ADMIN_API_SIGNATURE_TTL_SECONDS', 30),
6262
waitTimeSeconds: envInt('WAIT_SECONDS', 5),
6363
cookieKey: envString('COOKIE_KEY'),
6464
interactionCookieSameSite: envEnum(

packages/auth/src/shared/utils.test.ts

+11-6
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ describe('utils', (): void => {
9191
)
9292

9393
const timestamp = signature.split(', ')[0].split('=')[1]
94-
const now = new Date((Number(timestamp) + 60) * 1000)
94+
const now = new Date(
95+
Number(timestamp) + (Config.adminApiSignatureTtlSeconds + 1) * 1000
96+
)
9597
jest.useFakeTimers({ now })
9698
const ctx = createContext<AppContext>(
9799
{
@@ -120,11 +122,7 @@ describe('utils', (): void => {
120122
Config.adminApiSignatureVersion,
121123
requestBody
122124
)
123-
const key = `signature:${signature}`
124-
const op = redis.multi()
125-
op.set(key, signature)
126-
op.expire(key, Config.adminApiSignatureTtl * 1000)
127-
await op.exec()
125+
128126
const ctx = createContext<AppContext>(
129127
{
130128
headers: {
@@ -138,6 +136,13 @@ describe('utils', (): void => {
138136
)
139137
ctx.request.body = requestBody
140138

139+
await expect(
140+
verifyApiSignature(ctx, {
141+
...Config,
142+
adminApiSecret: 'test-secret'
143+
})
144+
).resolves.toBe(true)
145+
141146
const verified = await verifyApiSignature(ctx, {
142147
...Config,
143148
adminApiSecret: 'test-secret'

packages/auth/src/shared/utils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ async function canApiSignatureBeProcessed(
7171
config: IAppConfig
7272
): Promise<boolean> {
7373
const { timestamp } = getSignatureParts(signature)
74-
const signatureTime = Number(timestamp) * 1000
74+
const signatureTime = Number(timestamp)
7575
const currentTime = Date.now()
76-
const ttlMilliseconds = config.adminApiSignatureTtl * 1000
76+
const ttlMilliseconds = config.adminApiSignatureTtlSeconds * 1000
7777

7878
if (currentTime - signatureTime > ttlMilliseconds) return false
7979

packages/auth/src/tests/apiSignature.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export function generateApiSignature(
77
// eslint-disable-next-line @typescript-eslint/no-explicit-any
88
body: any
99
): string {
10-
const timestamp = Math.round(new Date().getTime() / 1000)
10+
const timestamp = Date.now()
1111
const payload = `${timestamp}.${canonicalize(body)}`
1212
const hmac = createHmac('sha256', secret)
1313
hmac.update(payload)

packages/backend/src/config/app.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export const Config = {
161161

162162
adminApiSecret: process.env.API_SECRET, // optional
163163
adminApiSignatureVersion: envInt('API_SIGNATURE_VERSION', 1),
164-
adminApiSignatureTtl: envInt('ADMIN_API_SIGNATURE_TTL_SECONDS', 30),
164+
adminApiSignatureTtlSeconds: envInt('ADMIN_API_SIGNATURE_TTL_SECONDS', 30),
165165

166166
keyId: envString('KEY_ID'),
167167
privateKey: privateKeyFileValue,

packages/backend/src/shared/utils.test.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,9 @@ describe('utils', (): void => {
204204
)
205205

206206
const timestamp = signature.split(', ')[0].split('=')[1]
207-
const now = new Date((Number(timestamp) + 60) * 1000)
207+
const now = new Date(
208+
Number(timestamp) + (Config.adminApiSignatureTtlSeconds + 1) * 1000
209+
)
208210
jest.useFakeTimers({ now })
209211
const ctx = createContext<AppContext>(
210212
{
@@ -233,11 +235,6 @@ describe('utils', (): void => {
233235
Config.adminApiSignatureVersion,
234236
requestBody
235237
)
236-
const key = `signature:${signature}`
237-
const op = redis.multi()
238-
op.set(key, signature)
239-
op.expire(key, Config.adminApiSignatureTtl * 1000)
240-
await op.exec()
241238
const ctx = createContext<AppContext>(
242239
{
243240
headers: {
@@ -251,6 +248,13 @@ describe('utils', (): void => {
251248
)
252249
ctx.request.body = requestBody
253250

251+
await expect(
252+
verifyApiSignature(ctx, {
253+
...Config,
254+
adminApiSecret: 'test-secret'
255+
})
256+
).resolves.toBe(true)
257+
254258
const verified = await verifyApiSignature(ctx, {
255259
...Config,
256260
adminApiSecret: 'test-secret'

packages/backend/src/shared/utils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,9 @@ async function canApiSignatureBeProcessed(
153153
config: IAppConfig
154154
): Promise<boolean> {
155155
const { timestamp } = getSignatureParts(signature)
156-
const signatureTime = Number(timestamp) * 1000
156+
const signatureTime = Number(timestamp)
157157
const currentTime = Date.now()
158-
const ttlMilliseconds = config.adminApiSignatureTtl * 1000
158+
const ttlMilliseconds = config.adminApiSignatureTtlSeconds * 1000
159159

160160
if (currentTime - signatureTime > ttlMilliseconds) return false
161161

packages/backend/src/tests/apiSignature.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export function generateApiSignature(
77
// eslint-disable-next-line @typescript-eslint/no-explicit-any
88
body: any
99
): string {
10-
const timestamp = Math.round(new Date().getTime() / 1000)
10+
const timestamp = Date.now()
1111
const payload = `${timestamp}.${canonicalize(body)}`
1212
const hmac = createHmac('sha256', secret)
1313
hmac.update(payload)

packages/backend/src/tests/quote.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,7 @@ export async function createQuote(
165165
maxPacketAmount: BigInt('9223372036854775807')
166166
})
167167

168-
const withGraphFetchedArray = [
169-
'asset',
170-
'walletAddress',
171-
'walletAddress.asset'
172-
]
168+
const withGraphFetchedArray = ['asset', 'walletAddress.asset']
173169
if (withFee) {
174170
withGraphFetchedArray.push('fee')
175171
}

packages/backend/src/webhook/service.test.ts

+27-10
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ describe('Webhook Service', (): void => {
7373
})
7474

7575
afterEach(async (): Promise<void> => {
76+
jest.useRealTimers()
7677
await truncateTables(knex)
7778
})
7879

@@ -317,16 +318,6 @@ describe('Webhook Service', (): void => {
317318
): Scope {
318319
return nock(webhookUrl.origin)
319320
.post(webhookUrl.pathname, function (this: Definition, body) {
320-
assert.ok(this.headers)
321-
const headers = this.headers as Record<string, ReplyHeaderValue>
322-
const signature = headers['rafiki-signature']
323-
expect(
324-
generateWebhookSignature(
325-
body,
326-
WEBHOOK_SECRET,
327-
Config.signatureVersion
328-
)
329-
).toEqual(signature)
330321
expect(body).toMatchObject({
331322
id: expectedEvent.id,
332323
type: expectedEvent.type,
@@ -356,6 +347,32 @@ describe('Webhook Service', (): void => {
356347
})
357348
})
358349

350+
test('Signs webhook event', async (): Promise<void> => {
351+
jest.useFakeTimers({
352+
now: Date.now(),
353+
advanceTimers: true // needed for nock when using fake timers
354+
})
355+
356+
const scope = nock(webhookUrl.origin)
357+
.post(webhookUrl.pathname, function (this: Definition, body) {
358+
assert.ok(this.headers)
359+
const headers = this.headers as Record<string, ReplyHeaderValue>
360+
const signature = headers['rafiki-signature']
361+
expect(
362+
generateWebhookSignature(
363+
body,
364+
WEBHOOK_SECRET,
365+
Config.signatureVersion
366+
)
367+
).toEqual(signature)
368+
return true
369+
})
370+
.reply(200)
371+
372+
await expect(webhookService.processNext()).resolves.toEqual(event.id)
373+
scope.done()
374+
})
375+
359376
test.each([[201], [400], [504]])(
360377
'Schedules retry if request fails (%i)',
361378
async (status): Promise<void> => {

packages/backend/src/webhook/service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export function generateWebhookSignature(
230230
secret: string,
231231
version: number
232232
): string {
233-
const timestamp = Math.round(new Date().getTime() / 1000)
233+
const timestamp = Date.now()
234234

235235
const payload = `${timestamp}.${canonicalize({
236236
id: event.id,

packages/frontend/app/lib/apollo.server.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ BigInt.prototype.toJSON = function (this: bigint) {
2929
const authLink = setContext((request, { headers }) => {
3030
if (!process.env.SIGNATURE_SECRET || !process.env.SIGNATURE_VERSION)
3131
return { headers }
32-
const timestamp = Math.round(new Date().getTime() / 1000)
32+
const timestamp = Date.now()
3333
const version = process.env.SIGNATURE_VERSION
3434

3535
const { query, variables, operationName } = request

pnpm-lock.yaml

+7-14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/integration/lib/apollo-client.ts

+45-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,54 @@
11
import type { NormalizedCacheObject } from '@apollo/client'
2-
import { ApolloClient, InMemoryCache } from '@apollo/client'
2+
import {
3+
ApolloClient,
4+
InMemoryCache,
5+
createHttpLink,
6+
ApolloLink
7+
} from '@apollo/client'
8+
import { setContext } from '@apollo/client/link/context'
9+
import { print } from 'graphql/language/printer'
10+
import { createHmac } from 'crypto'
11+
import { canonicalize } from 'json-canonicalize'
312

4-
export function createApolloClient(
13+
interface CreateApolloClientArgs {
514
graphqlUrl: string
15+
signatureSecret?: string
16+
signatureVersion?: string
17+
}
18+
19+
export function createApolloClient(
20+
args: CreateApolloClientArgs
621
): ApolloClient<NormalizedCacheObject> {
22+
const httpLink = createHttpLink({
23+
uri: args.graphqlUrl
24+
})
25+
26+
const authLink = setContext((request, { headers }) => {
27+
if (!args.signatureSecret || !args.signatureVersion) return { headers }
28+
const timestamp = Date.now()
29+
30+
const { query, variables, operationName } = request
31+
const formattedRequest = {
32+
variables,
33+
operationName,
34+
query: print(query)
35+
}
36+
37+
const payload = `${timestamp}.${canonicalize(formattedRequest)}`
38+
const hmac = createHmac('sha256', args.signatureSecret)
39+
hmac.update(payload)
40+
const digest = hmac.digest('hex')
41+
return {
42+
headers: {
43+
...headers,
44+
signature: `t=${timestamp}, v${args.signatureVersion}=${digest}`
45+
}
46+
}
47+
})
48+
749
return new ApolloClient({
8-
uri: graphqlUrl,
950
cache: new InMemoryCache(),
51+
link: ApolloLink.from([authLink, httpLink]),
1052
defaultOptions: {
1153
query: {
1254
fetchPolicy: 'no-cache'

0 commit comments

Comments
 (0)