-
-
Notifications
You must be signed in to change notification settings - Fork 832
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: more flexible argument extraction and signature testing #707
base: master
Are you sure you want to change the base?
Conversation
Added a new "source": "template" argument type, that evaluates a Go text/template against a context containing the request Body, Query, Payload and Headers, enabling much richer mapping from request attributes to argument parameters.
When you want to use "source":"template" arguments inside your hook file but also parse the hook file itself as a template, it is necessary use different delimiters on the two template parsers to avoid having to pepper all the inner templates with constructs like {{"{{"}}. Added an extra command line argument -template-delims that expects a comma-separated pair of delimiters like '[[,]]' that will be used when parsing the whole hooks file template. Inner templates in an Argument always use the default double-brace style.
Move the signature checking rules out of MatchRule into their own dedicated SignatureRule, configured as "check-signature" in the hooks file. This takes an algorithm, secret and Argument giving the source of the signature, and by default behaves exactly like the old payload-hmac-<algorithm> match rules. However it can also take a second optional Argument to customize how to generate the "string to sign", allowing signatures to be computed over something other than the full request body content. This could be a single header or payload item but more likely will be a "template" argument to combine items from different places in the request, such as the body content and one or more headers, e.g. to compute a signature over the X-Request-Id header, Date header, and request body, concatenated with CRLF, you could specify check-signature: algorithm: sha512 secret: 5uper5eecret signature: source: header name: X-Hook-Signature string-to-sign: source: template name: | {{- printf "%s\r\n" (.GetHeader "x-request-id") -}} {{- printf "%s\r\n" (.GetHeader "date") -}} {{- .BodyText -}}
There are other places in the logic that depend on errors extracting Argument values being of type ParameterNodeError specifically, so we shouldn't wrap these errors further.
I haven't had much time to commit to webhook in a while, but this PR got my attention. Kudos for making a great PR and explaining the situation well. Regarding templates, see this discussion. I feel like HCL is probably too complex and may hem us in in some areas. For purely adding a template language to our existing configs, I'd vote for I'm curious if we could solve the various hmac scenarios with templates instead of extending the config schema with the |
Iterating on this idea using expr while trying to stick with the # Assumes the hmac() function could derive the hash function and secret from the match stanza.
- match:
type: hmac-sha256
secret: secret
parameter:
source: expr
name: |
hmac($bodyText + ":" + getHeader("x-request-id")) == trimPrefix(getHeader("x-signature"), "sha256:")
- match:
type: value
value: true
parameter:
source: expr
name: |
date(getHeader("date")) > now() - duration("15m") |
I've been wanting to implement this for a while now, but never got the necessary focus time... I agree with @moorereason that some generic expressions format would be a more human readable format compared to Go templates, but either way it would be a long awaited huuuge improvement to webhook. At my full time job, I used to work a lot with mapbox mapping SDK which includes expressions for data styling, the bottom line is that it was very simple to learn and very powerful to use. Here's a documentation page for those, maybe it could serve as a source of inspiration for this. |
This started out as a more general fix for #643 but it has evolved into a larger refactor with a number of open questions, so I'm submitting it as a draft PR initially for discussion:
Overview
Issue #643 discusses the idea of verifying a signature that was computed over something other than just the request body content, and the related PR #644 provides an implementation of one special case of this general idea, concatenating one or more request header values to the end of the body data before calculating the signature. However there are many variations on this theme, for example
In this PR I've tried to come up with a general way to support cases like those as well.
The implementation is in two parts:
Argument
type"source": "template"
, where the argument value is computed by evaluating a Go template against a context derived from theRequest
. This lets you combine values from different headers, take substrings, etc.payload-hmac-*
"match" rules into a new "check-signature" rule type, which gives the option to use anArgument
for the "string-to-sign" as well as the one for the reference signature value.Together these let you compute signatures over any combination of the body, headers and query parameters, concatenated together with no delimiter, newlines, colons, etc. as required by your particular webhook sender.
Template argument type
The template argument type evaluates a Go template to build the argument value. The template context gives access to all the things that the other argument types provide, including
.Body
as a[]byte
,.BodyText
as a string).Payload
).Query
).Headers
, but also the.GetHeader "name"
function in the example above that canonicalises the header name the same way as a "source": "header" argument would do).ID
).RemoteAddr
,.ContentType
and.Method
)I've deliberately not exposed the whole of the
RawRequest
from the internalRequest
struct.The
check-signature
rule typeI've refactored all the
payload-hmac-sha1/256/512
match types into a new kind of rule. The oldis now expressed as
However, as well as "algorithm", "secret" and "signature", you can now specify "string-to-sign" as another argument, which could be a simple argument like a single header
or it could be a template argument like the example above
Backwards compatibility
I've made sure that existing hooks files (YAML and JSON) will still parse as before, with the
payload-hmac-*
match rules being converted tocheck-signature
rules during the loading process. The only wrinkle is if you want to start using templates in arguments and you need to parse the hooks file itself as a template, then it can get quite fiddly to escape the runtime templates from the parse-time engine. To mitigate this I've added a-template-delims
CLI option to change the delimiters used for the parse-time template engine, so you can say-template-delims='[[,]]'
and then use[[ getenv "..." ]]
for the parse-time template and{{ .GetHeader "..." }}
for the runtime ones.Open questions for discussion
Are Go Templates the right paradigm?
Go templates are very flexible, but should we stick with something simpler and with fewer gotchas around whitespace handling etc.? #512 mentions cel-go as an alternative.
Template context
Is the template context appropriate? Are there other things we could/should expose to the templates, or more sensible names for the things that are already there?
Do we want to add some more useful functions to the template engine? One extreme would be to introduce something like Sprig, the same function library used by Helm. This includes functions for things like date parsing and formatting, which would also open up a way to use template arguments to check things like "is the Date header less than 15 minutes old":
Though something like that probably deserves its own dedicated rule type.