dhensby / node-http-message-signatures

A node package for signing HTTP messages as per the http-message-signing draft specification
ISC License
13 stars 8 forks source link

Provide verification example #156

Closed timkelty closed 8 months ago

timkelty commented 8 months ago

I'm attempting to use this library to verify and request signed elsewhere, but struggling to get it working without an example.

dhensby commented 8 months ago

Are you using httpbis or cavage?

Are the tests helpful as an example?

timkelty commented 8 months ago

@dhensby ah, didn't think to look at tests – will give that a try.

Are you using httpbis or cavage?

httpbis, but figuring out the import was a bit of a stuggle as well, as the signing example in the readme doesn't appear accurate…

I suppose I should also ask – should it possible to use this in a browser-ish environment vs node? In my case, I'm a cloudflare worker, and am trying to verify a hmac-sha256 signature. Seems like this should be possible with my own verifier and SubtleCrypto, which is available.

FWIW, I've also tried this package, which has specific examples for verification in browser. While they have docs, they aren't updated, and I have also been unable to successfully verify using that.

dhensby commented 8 months ago

I guess it's time for me to update the docs!

It should work in the browser, I think, as there isn't anything requiring specific node functionality.

As you say, if you provide your own signer/verifier then it can all work off of Subtle etc.

Of course, HMAC in the browser isn't a good idea because you'll need to expose the secret to the client (I know you are talking about cloudflare workers, but this is just an NB incase others come and read and miss that detail).

timkelty commented 8 months ago

Here's what I have implemented, but cannot get it to verify:

Given secret: 123456789 Given request (generated via https://httpsig.org/):

HEAD / HTTP/1.1
Host: spoke-chain-redux-main-396d681a.preview.craftstaging.cloud
Signature-Input: sig=("host" "@method");alg="hmac-sha256";keyid="test-key"
Signature: sig=:IrkNB32AXmg1bdgPh9QqzCZRe8hMX+73w/eUN0KWP2c=:

Verifying here, but always coming back invalid. Any ideas what I'm missing https://gist.github.com/timkelty/f4270294a3861cefcaf732a02cd17d79

dhensby commented 8 months ago

@timkelty I've opened #157 which has some examples.

I think maybe the consistent use of Buffer objects in the library may mean that this can't actually compile in browsers, but it should run in environments that have a skeleton support for buffers (and it seems edge workers do: https://developers.cloudflare.com/workers/runtime-apis/nodejs/buffer/)

dhensby commented 8 months ago

@timkelty here's an example that validates the example request you gave me:

const { httpbis: { verifyMessage }, createVerifier } = require('http-message-signatures');

(async () => {
    const keys = new Map();
    keys.set('test-key', {
        id: 'test-key',
        algs: ['hmac-sha256'],
        verify: createVerifier('123456789', 'hmac-sha256'),
    });
    // minimal verification
    const verified = await verifyMessage({
        // logic for finding a key based on the signature parameters
        async keyLookup(params) {
            const keyId = params.keyid;
            // lookup key somehow
            return keys.get(keyId);
        },
        tolerance: Infinity,
    }, {
        method: 'HEAD',
        url: 'https://spoke-chain-redux-main-396d681a.preview.craftstaging.cloud',
        headers: {
            'host': 'spoke-chain-redux-main-396d681a.preview.craftstaging.cloud',
            'content-type': 'application/json',
            'content-digest': 'sha-512=:YMAam51Jz/jOATT6/zvHrLVgOYTGFy1d6GJiOHTohq4yP+pgk4vf2aCsyRZOtw8MjkM7iw7yZ/WkppmM44T3qg==:',
            'content-length': '123',
            'signature': 'sig=:IrkNB32AXmg1bdgPh9QqzCZRe8hMX+73w/eUN0KWP2c=:',
            'signature-input': 'sig=("host" "@method");alg="hmac-sha256";keyid="test-key"',
        },
    });
    console.log(verified);
})().catch(console.error);

Raw JS, I know (sorry) but it does come out as valid. I'll look at your gist to see if there's anything obvious.

dhensby commented 8 months ago

I think this is it!

import {Env} from './types';
import {
    httpbis,
    createVerifier,
} from 'http-message-signatures';
import {SignatureParameters} from "http-message-signatures/lib/types";

export default {
    async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
        const verifier = createVerifier('123456789', 'hmac-sha256');
        const keyLookup = (parameters: SignatureParameters) => {
            return Promise.resolve({
                id: 'test-key',
                algs: ['hmac-sha256'],
-               verifier,
+               verify: verifier,
            });
        };

        const valid = await httpbis.verifyMessage({
            keyLookup,
        }, request);

        if (!valid) {
            return new Response('Invalid signature', {
                status: 403,
            });
        }

        return new Response(null, {
            status: 200,
        });
    },
};
timkelty commented 8 months ago

doh! I wonder why TS wasn't yelling at me?! Or maybe I just missed it. Thanks so much! ❤️

timkelty commented 8 months ago

@dhensby So, that still isn't verifying for me in the CF worker env at least… Is there any option for just providing your own verify fn, similar to what this lib does? https://ltonetwork.github.io/http-message-signatures/verification/browser.html#verify-callback

Then, I could do my own crypto.subtle.verify logic to ensure it is compatible with my env.

dhensby commented 8 months ago

@timkelty see #157

timkelty commented 8 months ago

@dhensby getting closer – but even with a custom verifier, I still get ReferenceError: Buffer is not defined, so I suspect it is still being used somewhere behind the scenes?

Buffer is technically available in CF workers, but you have to import it as , so I'll have to see if I can polyfill it in my Vite/rollup config. Ideally, though, bringing my own verifier wouldn't require Buffer in my env.

https://gist.github.com/timkelty/f4270294a3861cefcaf732a02cd17d79 has my current attempt based on your #157 example, with these changes:

As you can see, no references to Buffer, but I'm still getting ReferenceError: Buffer is not defined.

Update:

I got past the Buffer bump with vite-plugin-node-polyfills, but now hit: TypeError: ft.verify is not a function

dhensby commented 8 months ago

Buffers: Yes, the library does heavily rely on Buffer internally. I suppose I could replace that with Uint8Array, etc but that means also implementing custom base64 decode/encode which I don't currently have a desire to do.

crypot.subtle.verify - yes, good point that would be better and also means the examples are easier to adjust for RSA/ECDSA keys; I've updated the PR.

ft.verify is not a function - so that's happening because my example is confusing. I've named the custom verifier createMyVerifier() which implies it is a drop-in replacement for the internal createVerifier() function, but it isn't - it's just the verify function. I'll rework the example so it is a bit clearer.

dhensby commented 8 months ago

Just to say, this change should work:


import {httpbis} from 'http-message-signatures';

function createMyVerifier() {
    return {
        id: 'test-key',
        algs: ['hmac-sha256'],
        async verify(data, signature, parameters) {
            const keyData = new TextEncoder().encode('123456789');
            const algorithm = { name: 'HMAC', hash: 'SHA-256' };
            const key = await crypto.subtle.importKey('raw', keyData, algorithm, false, ['verify']);
            const encodedData = new TextEncoder().encode(data);
            return await crypto.subtle.verify('HMAC', key, signature, encodedData);
        },
    };
}

(async () => {
    // an example keystore for looking up keys by ID
    const keys = new Map();
-   keys.set('test-key', {
-       id: 'test-key',
-       algs: ['hmac-sha256'],
-       // as with signing, you can provide your own verifier here instead of using the built-in helpers
-       verify: createMyVerifier(),
-   });
+   keys.set('test-key', createMyVerifier());
    // minimal verification
    const verified = await httpbis.verifyMessage({
        // logic for finding a key based on the signature parameters
        async keyLookup(params) {
            const keyId = params.keyid;
            // lookup and return key - note, we could also lookup using the alg too (`params.alg`)
            // if there is no key, `verifyMessage()` will throw an error
            return keys.get(keyId);
        },
    }, {
        method: 'HEAD',
        url: 'https://spoke-chain-redux-main-396d681a.preview.craftstaging.cloud/',
        headers: {
            'host': 'spoke-chain-redux-main-396d681a.preview.craftstaging.cloud',
            'signature': 'sig=:IrkNB32AXmg1bdgPh9QqzCZRe8hMX+73w/eUN0KWP2c=:',
            'signature-input': 'sig=("host" "@method");alg="hmac-sha256";keyid="test-key"',
        },
    });
})().catch(console.error);
timkelty commented 8 months ago

because my example is confusing

Hah, that was my next question!

implementing custom base64 decode/encode

I've seen other libs use https://github.com/brianloveswords/base64url. That said, for my use, I've gotten around the Buffer requirement, so I'm all good.

Just to say, this change should work:

Indeed it did! 👯👯


Once I tried to replace the request object literal with the actual request, I discovered the request arg isn't compatible with Request or Headers, which is maybe expected, as those are web/browser APIs.

To address that:

{
  method: request.method,
  url: request.url,
  headers: Object.fromEntries(request.headers.entries()),
}

I'll post a full working Cloudflare workers example shortly in case you want to include it in your docs.

Thanks so much for all the help!

github-actions[bot] commented 8 months ago

:tada: This issue has been resolved in version 1.0.1 :tada:

The release is available on:

Your semantic-release bot :package::rocket: