Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions packages/http-signature-utils/src/utils/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ describe('Signature Verification', (): void => {
)

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"'}
title | sigInputHeader | withAuthorization
${'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"'} | ${true}
${'fails @method is missing'} | ${'sig1=("@target-uri" "content-digest" "content-length" "content-type");created=1618884473;keyid="gnap-key"'} | ${false}
${'fails if @target-uri is missing'} | ${'sig1=("@method" "content-digest" "content-length" "content-type");created=1618884473;keyid="gnap-key"'} | ${false}
${'fails if content-digest is missing while body is present'} | ${'sig1=("@method" "@target-uri" "content-length" "content-type");created=1618884473;keyid="gnap-key"'} | ${false}
${'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"'} | ${true}
`(
'validates signature header and $title',
async ({ sigInputHeader }): Promise<void> => {
async ({ sigInputHeader, withAuthorization }): Promise<void> => {
const testRequestBody = JSON.stringify({ foo: 'bar' })
const request = {
headers: {
Expand All @@ -73,7 +73,9 @@ describe('Signature Verification', (): void => {
]),
'content-length': '1234',
'signature-input': sigInputHeader,
authorization: 'GNAP test-access-token'
...(withAuthorization
? { authorization: 'GNAP test-access-token' }
: {})
},
method: 'GET',
url: 'http://example.com/test',
Expand Down
32 changes: 24 additions & 8 deletions packages/http-signature-utils/src/utils/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ function getSigInputComponents(sigInput: string): string[] | null {
: null
}

function requestHasBody(request: RequestLike): boolean {
if (request.body == null) return false
if (typeof request.body === 'string') return request.body.length > 0
if (typeof request.body === 'object') {
return Object.keys(request.body as object).length > 0
}
return true
}

function validateSigInputComponents(
sigInputComponents: string[],
request: RequestLike
Expand All @@ -97,18 +106,25 @@ function validateSigInputComponents(
if (component !== component.toLowerCase()) return false
}

const isValidContentDigest =
!sigInputComponents.includes('content-digest') ||
(!!request.headers['content-digest'] &&
const hasBody = requestHasBody(request)
// Open Payments / GNAP: when a body is present, content-digest MUST be covered
// and verified. Omitting it previously failed open (body could be swapped).
// Sibling of interledger/open-payments-go#50.
const isValidContentDigest = !hasBody
? !sigInputComponents.includes('content-digest') ||
(!!request.headers['content-digest'] &&
verifyContentDigest(
request.body as string,
request.headers['content-digest'] as string
))
: 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.body as string,
request.headers['content-digest'] as string
))
)

return !(
!isValidContentDigest ||
Expand Down