Verify HMAC Hash Using Cloudflare Workers

1.5k views Asked by At

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

1

There are 1 answers

0
markpirvine On BEST ANSWER

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
}