Skip to content

Commit

Permalink
Merge branch 'main' into bw-backend-openapi
Browse files Browse the repository at this point in the history
  • Loading branch information
wilsonianb committed Dec 1, 2022
2 parents 695817d + cbbc416 commit e568995
Show file tree
Hide file tree
Showing 48 changed files with 812 additions and 1,054 deletions.
4 changes: 4 additions & 0 deletions .github/codeql/source.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: "CodeQL source files config"

paths-ignore:
- '**/*.test.ts'
7 changes: 7 additions & 0 deletions .github/codeql/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: "CodeQL test files config"

query-filters:
- exclude:
id: js/hardcoded-credentials
paths:
- '**/*.test.ts'
4 changes: 4 additions & 0 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ jobs:
fail-fast: false
matrix:
language: [ 'javascript' ]
config:
- './.github/codeql/source.yml'
- './.github/codeql/tests.yml'
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
Expand All @@ -50,6 +53,7 @@ jobs:
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
config-file: ${{ matrix.config }}

# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"license": "Apache-2.0",
"repository": "https://github.com/interledger/rafiki",
"engines": {
"node": "16"
"node": "16 || 18"
},
"scripts": {
"preinstall": "npx only-allow pnpm",
Expand Down
1 change: 1 addition & 0 deletions packages/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@koa/router": "^12.0.0",
"ajv": "^8.11.0",
"axios": "^0.27.2",
"httpbis-digest-headers": "github:interledger/httpbis-digest-headers",
"jose": "^4.9.0",
"knex": "^0.95",
"koa": "^2.13.4",
Expand Down
70 changes: 54 additions & 16 deletions packages/auth/src/signature/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 { createTestApp, TestContainer } from '../tests/app'
import { truncateTables } from '../tests/tableManager'
Expand Down Expand Up @@ -58,6 +59,8 @@ describe('Signature Service', (): void => {
).resolves.toBe(true)
})

const testRequestBody = { foo: 'bar' }

test.each`
title | withAuthorization | withRequestBody
${''} | ${true} | ${true}
Expand All @@ -71,14 +74,17 @@ describe('Signature Service', (): void => {
const headers = {
'Content-Type': 'application/json'
}
let expectedChallenge = `"@method": GET\n"@target-uri": /test\n"content-type": application/json\n`
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'] = 'sha-256=:test-hash:'
headers['Content-Digest'] = contentDigest
headers['Content-Length'] = '1234'
expectedChallenge +=
'"content-digest": sha-256=:test-hash:\n"content-length": 1234\n'
expectedChallenge += `"content-digest": ${contentDigest}\n"content-length": 1234\n`
}

if (withAuthorization) {
Expand All @@ -98,13 +104,13 @@ describe('Signature Service', (): void => {
{
headers,
method: 'GET',
url: '/test'
url: 'example.com/test'
},
{},
deps
)

ctx.request.body = withRequestBody ? { foo: 'bar' } : {}
ctx.request.body = withRequestBody ? testRequestBody : {}

const challenge = sigInputToChallenge(sigInputHeader, ctx)

Expand All @@ -126,7 +132,10 @@ describe('Signature Service', (): void => {
{
headers: {
'Content-Type': 'application/json',
'Content-Digest': 'sha-256=:test-hash:',
'Content-Digest': createContentDigestHeader(
JSON.stringify(testRequestBody),
['sha-512']
),
'Content-Length': '1234',
'Signature-Input': sigInputHeader,
Authorization: 'GNAP test-access-token'
Expand All @@ -138,7 +147,7 @@ describe('Signature Service', (): void => {
deps
)

ctx.request.body = { foo: 'bar' }
ctx.request.body = testRequestBody
ctx.method = 'GET'
ctx.request.url = '/test'

Expand Down Expand Up @@ -234,7 +243,7 @@ describe('Signature Service', (): void => {
headers: {
Accept: 'application/json'
},
url: '/',
url: 'http://example.com/',
method: 'POST'
},
{},
Expand Down Expand Up @@ -268,7 +277,7 @@ describe('Signature Service', (): void => {
Accept: 'application/json',
Authorization: `GNAP ${grant.continueToken}`
},
url: '/continue',
url: 'http://example.com/continue',
method: 'POST'
},
{ id: grant.continueId },
Expand Down Expand Up @@ -298,7 +307,7 @@ describe('Signature Service', (): void => {
headers: {
Accept: 'application/json'
},
url: tokenManagementUrl,
url: 'http://example.com' + tokenManagementUrl,
method: 'DELETE'
},
{ id: managementId },
Expand Down Expand Up @@ -329,7 +338,7 @@ describe('Signature Service', (): void => {
headers: {
Accept: 'application/json'
},
url: tokenManagementUrl,
url: 'http://example.com' + tokenManagementUrl,
method
},
{ id: managementId },
Expand All @@ -341,10 +350,11 @@ describe('Signature Service', (): void => {
proof: 'httpsig',
resource_server: 'test'
}
await tokenHttpsigMiddleware(ctx, next)
expect(ctx.response.status).toEqual(400)
expect(ctx.response.body.error).toEqual('invalid_request')
expect(ctx.response.body.message).toEqual('invalid signature headers')
await expect(tokenHttpsigMiddleware(ctx, next)).rejects.toMatchObject({
status: 400,
error: 'invalid_request',
message: 'invalid signature headers'
})
})

test('middleware fails if signature is invalid', async (): Promise<void> => {
Expand Down Expand Up @@ -404,5 +414,33 @@ describe('Signature Service', (): void => {
message: 'invalid client'
})
})

test('middleware fails if content-digest is invalid', async (): Promise<void> => {
const ctx = await createContextWithSigHeaders(
{
headers: {
Accept: 'application/json'
},
url: '/',
method: 'POST'
},
{},
{
client: CLIENT
},
privateKey,
testClientKey.kid,
deps
)

ctx.request.body = { test: 'this is wrong' }

await expect(
grantInitiationHttpsigMiddleware(ctx, next)
).rejects.toMatchObject({
status: 400,
message: 'invalid signature headers'
})
})
})
})
39 changes: 17 additions & 22 deletions packages/auth/src/signature/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import * as crypto from 'crypto'
import { importJWK } from 'jose'
import { JWK } from 'open-payments'
import { verifyContentDigest } from 'httpbis-digest-headers'

import { AppContext } from '../app'
import { Grant } from '../grant/model'
Expand Down Expand Up @@ -105,12 +106,21 @@ function validateSigInputComponents(
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.request.body &&
Object.keys(ctx.request.body).length > 0 &&
!sigInputComponents.includes('content-digest')) ||
(ctx.headers['authorization'] &&
!sigInputComponents.includes('authorization'))
)
Expand All @@ -134,7 +144,7 @@ export function sigInputToChallenge(
if (component === '@method') {
signatureBase += `"@method": ${ctx.request.method}\n`
} else if (component === '@target-uri') {
signatureBase += `"@target-uri": ${ctx.request.url}\n`
signatureBase += `"@target-uri": ${ctx.request.href}\n`
} else {
signatureBase += `"${component}": ${ctx.headers[component]}\n`
}
Expand Down Expand Up @@ -178,12 +188,7 @@ export async function grantContinueHttpsigMiddleware(
next: () => Promise<any>
): Promise<void> {
if (!validateHttpSigHeaders(ctx)) {
ctx.status = 400
ctx.body = {
error: 'invalid_request',
message: 'invalid signature headers'
}
return
ctx.throw(400, 'invalid signature headers', { error: 'invalid_request' })
}

const continueToken = ctx.headers['authorization'].replace(
Expand Down Expand Up @@ -226,12 +231,7 @@ export async function grantInitiationHttpsigMiddleware(
next: () => Promise<any>
): Promise<void> {
if (!validateHttpSigHeaders(ctx)) {
ctx.status = 400
ctx.body = {
error: 'invalid_request',
message: 'invalid signature headers'
}
return
ctx.throw(400, 'invalid signature headers', { error: 'invalid_request' })
}

const { body } = ctx.request
Expand All @@ -251,12 +251,7 @@ export async function tokenHttpsigMiddleware(
next: () => Promise<any>
): Promise<void> {
if (!validateHttpSigHeaders(ctx)) {
ctx.status = 400
ctx.body = {
error: 'invalid_request',
message: 'invalid signature headers'
}
return
ctx.throw(400, 'invalid signature headers', { error: 'invalid_request' })
}

const accessTokenService = await ctx.container.use('accessTokenService')
Expand Down
25 changes: 14 additions & 11 deletions packages/auth/src/tests/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,17 @@ export async function createContextWithSigHeaders(
container?: IocContract<AppServices>
): Promise<AppContext> {
const { headers, url, method } = reqOpts
const { signature, sigInput, contentDigest } = await generateSigHeaders({
privateKey,
keyId,
url,
method,
optionalComponents: {
body: requestBody,
authorization: headers.Authorization as string
}
})
const { signature, sigInput, contentDigest, contentLength, contentType } =
await generateSigHeaders({
privateKey,
keyId,
url,
method,
optionalComponents: {
body: requestBody,
authorization: headers.Authorization as string
}
})

const ctx = createContext(
{
Expand All @@ -64,7 +65,9 @@ export async function createContextWithSigHeaders(
...headers,
'Content-Digest': contentDigest,
Signature: signature,
'Signature-Input': sigInput
'Signature-Input': sigInput,
'Content-Type': contentType,
'Content-Length': contentLength
}
},
params,
Expand Down
33 changes: 26 additions & 7 deletions packages/auth/src/tests/signature.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import crypto from 'crypto'
import { v4 } from 'uuid'
import { createContentDigestHeader } from 'httpbis-digest-headers'
import { importJWK, exportJWK } from 'jose'
import { JWKWithRequired } from '../client/service'

Expand Down Expand Up @@ -68,21 +69,33 @@ export async function generateSigHeaders({
body?: unknown
authorization?: string
}
}): Promise<{ sigInput: string; signature: string; contentDigest?: 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"'
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) {
const hash = crypto.createHash('sha256')
hash.update(Buffer.from(JSON.stringify(body)))
const bodyDigest = hash.digest()
contentDigest = `sha-256:${bodyDigest.toString('base64')}:`
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) {
Expand All @@ -94,5 +107,11 @@ export async function generateSigHeaders({
const privateJwk = (await importJWK(privateKey)) as crypto.KeyLike
const signature = crypto.sign(null, Buffer.from(challenge), privateJwk)

return { signature: signature.toString('base64'), sigInput, contentDigest }
return {
signature: signature.toString('base64'),
sigInput,
contentDigest,
contentLength,
contentType
}
}
Loading

0 comments on commit e568995

Please sign in to comment.