diff --git a/packages/http-signature-utils/src/utils/validation.test.ts b/packages/http-signature-utils/src/utils/validation.test.ts index aacd931..37fcfee 100644 --- a/packages/http-signature-utils/src/utils/validation.test.ts +++ b/packages/http-signature-utils/src/utils/validation.test.ts @@ -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 => { + async ({ sigInputHeader, withAuthorization }): Promise => { const testRequestBody = JSON.stringify({ foo: 'bar' }) const request = { headers: { @@ -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', diff --git a/packages/http-signature-utils/src/utils/validation.ts b/packages/http-signature-utils/src/utils/validation.ts index c021907..8ad2d47 100644 --- a/packages/http-signature-utils/src/utils/validation.ts +++ b/packages/http-signature-utils/src/utils/validation.ts @@ -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 @@ -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 ||