2

I'm trying to verify a HMAC signature received from a WebHook. The details of the WebHook are https://cloudconvert.com/api/v2/webhooks#webhooks-events

This says that the HMAC is generated using hash_hmac (PHP) and is a SHA256 hash of the body - which is JSON. An example received is:

c4faebbfb4e81db293801604d0565cf9701d9e896cae588d73ddfef3671e97d7

This looks like lowercase hexits.

I'm trying to use Cloudflare Workers to process the request, however I can't verify the hash. My code is below:

const encoder = new TextEncoder()

addEventListener('fetch', event => {
    event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
    const contentType = request.headers.get('content-type') || ''
    const signature = request.headers.get('CloudConvert-Signature')
    let data

    await S.put('HEADER', signature)

    if (contentType.includes('application/json')) {
        data = await request.json()
        await S.put('EVENT', data.event)
        await S.put('TAG', data.job.tag)
        await S.put('JSON', JSON.stringify(data))
    }

    const key2 = await crypto.subtle.importKey(
        'raw',
        encoder.encode(CCSigningKey2),
        { name: 'HMAC', hash: 'SHA-256' },
        false,
        ['sign']
    )

    const signed2 = await crypto.subtle.sign(
        'HMAC',
        key2,
        encoder.encode(JSON.stringify(data))
    )
    
    await S.put('V22', btoa(String.fromCharCode(...new Uint8Array(signed2))))

    return new Response(null, {
        status: 204,
        headers: {
            'Cache-Control': 'no-cache'
        }
    })
}

This will generate a hash of:

e52613e6ecebdf98bb085f04ca1f91bf9a5cf1dc085f89dcaa3e5fbf5ebf1b06

I've tried use the crypto.subtle.verify method, but that didn't work.

Can anyone see any issues with the code? Or have done this successfully using Cloudflare Workers?

Mark

markpirvine
  • 1,485
  • 1
  • 23
  • 54
  • Does the code work as expected if executed in a browser, rather than Cloudflare Workers? If so, this is probably a Workers bug. If it doesn't work in a browser, then your question is probably more about WebCrypto API usage in general than a bout Cloudflare Workers specifically. – Kenton Varda Jun 07 '21 at 17:33
  • No, I'm still unable to get the hash to match even outside of Cloudflare Workers. I've updated my tags – markpirvine Jun 07 '21 at 19:55

1 Answers1

1

I finally got this working using the verify method (I had previously tried the verify method, but it didn't work). The main problem seems to the use of request.json() wrapped in JSON.stringify. Changing this to request.text() resolved the issue. I can then use JSON.parse to access the data after verifying the signature. The code is as follows:

const encoder = new TextEncoder()

addEventListener('fetch', event => {
    event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
    const signature = request.headers.get('CloudConvert-Signature')

    const key = await crypto.subtle.importKey(
        'raw',
        encoder.encode(CCSigningKey2),
        { name: 'HMAC', hash: 'SHA-256' },
        false,
        ['verify']
    )

    const data = await request.text()

    const verified = await crypto.subtle.verify(
        'HMAC',
        key,
        hexStringToArrayBuffer(signature),
        encoder.encode(data)
    )

    if (!verified) {
        return new Response('Verification failed', {
            status: 401,
            headers: {
                'Cache-Control': 'no-cache'
            }
        })
    }

    return new Response(null, {
        status: 204,
        headers: {
            'Cache-Control': 'no-cache'
        }
    })
}

function hexStringToArrayBuffer(hexString) {
    hexString = hexString.replace(/^0x/, '')

    if (hexString.length % 2 != 0) {
        return
    }

    if (hexString.match(/[G-Z\s]/i)) {
        return
    }

    return new Uint8Array(
        hexString.match(/[\dA-F]{2}/gi).map(function(s) {
            return parseInt(s, 16)
        })
    ).buffer
}
markpirvine
  • 1,485
  • 1
  • 23
  • 54