From a39798803ae674e401b8ca94083b461f3aab9623 Mon Sep 17 00:00:00 2001 From: Traky Deng Date: Wed, 9 Oct 2024 16:02:55 +0800 Subject: [PATCH] docs: update `hmac-auth` plugin doc per the refactoring (#11627) --- docs/en/latest/plugins/hmac-auth.md | 638 +++++++++++++++++---------- docs/zh/latest/plugins/hmac-auth.md | 654 +++++++++++++++++----------- 2 files changed, 801 insertions(+), 491 deletions(-) diff --git a/docs/en/latest/plugins/hmac-auth.md b/docs/en/latest/plugins/hmac-auth.md index 9a7fadc8aa36..5ba08c05b127 100644 --- a/docs/en/latest/plugins/hmac-auth.md +++ b/docs/en/latest/plugins/hmac-auth.md @@ -30,31 +30,37 @@ description: This document contains information about the Apache APISIX hmac-aut ## Description -The `hmac-auth` Plugin adds [HMAC authentication](https://en.wikipedia.org/wiki/HMAC) to a Route or a Service. +The `hmac-auth` plugin supports HMAC (Hash-based Message Authentication Code) authentication as a mechanism to ensure the integrity of requests, preventing them from being modified during transmissions. To use the plugin, you would configure HMAC secret keys on [Consumers](../terminology/consumer.md) and enable the plugin on Routes or Services. -This Plugin works with a [Consumer](../terminology/consumer.md) object and a consumer of your API has to add its key to the request header for verification. +Once enabled, the plugin verifies the HMAC signature in the request's `Authorization` header and check that incoming requests are from trusted sources. Specifically, when APISIX receives an HMAC-signed request, the key ID is extracted from the `Authorization` header. APISIX then retrieves the corresponding consumer configuration, including the secret key. If the key ID is valid and exists, APISIX generates an HMAC signature using the request's `Date` header and the secret key. If this generated signature matches the signature provided in the `Authorization` header, the request is authenticated and forwarded to upstream services. + +The plugin implementation is based on [draft-cavage-http-signatures](https://www.ietf.org/archive/id/draft-cavage-http-signatures-12.txt). ## Attributes +The following attributes are available for configurations on Consumers or Credentials. + | Name | Type | Required | Default | Valid values | Description | |-----------------------|---------------|----------|---------------|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| access_key | string | True | | | Unique key of a Consumer. If different Consumers have the same key, a request matching exception will occur. | -| secret_key | string | True | | | Used in pair with `access_key`. This field supports saving the value in Secret Manager using the [APISIX Secret](../terminology/secret.md) resource. | -| algorithm | string | False | "hmac-sha256" | ["hmac-sha1", "hmac-sha256", "hmac-sha512"] | Encryption algorithm used. | -| clock_skew | integer | False | 0 | | Clock skew allowed by the signature in seconds. Setting it to `0` will skip checking the date. | -| signed_headers | array[string] | False | | | List of headers to be used in the encryption algorithm. If specified, the client request can only contain the specified headers. When unspecified, all the headers are used in the encryption algorithm. | -| keep_headers | boolean | False | false | [ true, false ] | When set to `true`, keeps the request headers `X-HMAC-SIGNATURE`, `X-HMAC-ALGORITHM` and `X-HMAC-SIGNED-HEADERS` in the HTTP request after successful authentication. Otherwise, the headers are removed. | -| encode_uri_params | boolean | False | true | [ true, false ] | When set to `true` encodes the URI parameters. For example, `params1=hello%2Cworld` is encoded whereas, `params2=hello,world` is not. | -| validate_request_body | boolean | False | false | [ true, false ] | When set to `true`, validates the request body. | -| max_req_body | integer | False | 512 * 1024 | | Max size of the request body to allow. | +| key_id | string | True | | | Unique identifier for the consumer, which identifies the associated configurations such as the secret key. | +| secret_key | string | True | | | Secret key used to generate an HMAC. This field supports saving the value in Secret Manager using the [APISIX Secret](../terminology/secret.md) resource. | -NOTE: `encrypt_fields = {"secret_key"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields). +The following attributes are available for configurations on Routes or Services. + +| Name | Type | Required | Default | Valid values | Description | +|-----------------------|---------------|----------|---------------|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| allowed_algorithms | array[string] | False | ["hmac-sha1", "hmac-sha256", "hmac-sha512"] | combination of "hmac-sha1", "hmac-sha256", and "hmac-sha512" | The list of HMAC algorithms allowed. | +| clock_skew | integer | False | 300 | >=1 | Maximum allowable time difference in seconds between the client request's timestamp and APISIX server's current time. This helps account for discrepancies in time synchronization between the client’s and server’s clocks and protect against replay attacks. The timestamp in the Date header (must be in GMT format) will be used for the calculation. | +| signed_headers | array[string] | False | | | The list of HMAC-signed headers that should be included in the client request's HMAC signature. | +| validate_request_body | boolean | False | false | | If true, validate the integrity of the request body to ensure it has not been tampered with during transmission. Specifically, the plugin creates a SHA-256 base64-encoded digest and compare it to the `Digest` header. If the Digest` header is missing or if the digests do not match, the validation fails. | +| hide_credentials | boolean | False | false | | If true, do not pass the authorization request header to upstream services. | -## Enable Plugin +NOTE: `encrypt_fields = {"secret_key"}` is also defined in the schema, which means that the field will be stored encrypted in etcd. See [encrypted storage fields](../plugin-develop.md#encrypted-storage-fields). -First we enable the Plugin on a Consumer object as shown below: +## Examples :::note + You can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command: ```bash @@ -63,312 +69,474 @@ admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"/ ::: +Before proceeding, create a sample consumer and configure its credential, which will be used for all examples below. + +Create a consumer `john`: + ```shell -curl http://127.0.0.1:9180/apisix/admin/consumers -H "X-API-KEY: $admin_key" -X PUT -d ' -{ - "username": "jack", - "plugins": { - "hmac-auth": { - "access_key": "user-key", - "secret_key": "my-secret-key", - "clock_skew": 0, - "signed_headers": ["User-Agent", "Accept-Language", "x-custom-a"] - } +curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -d '{ + "username": "john" } -}' + }' ``` -You can also use the [APISIX Dashboard](/docs/dashboard/USER_GUIDE) to complete the operation through a web UI. +Create `hmac-auth` credential for `john`: - +### Implement HMAC Authentication on a Route + +The following example shows how to implement HMAC authentications on a route using the most minimal configurations. -Next, you can configure the Plugin to a Route or a Service: +Create a route with the `hmac-auth` plugin using its default configurations: ```shell -curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X PUT -d ' -{ - "uri": "/index.html", +curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -d '{ + "id": "hmac-auth-route", + "uri": "/get", + "methods": ["GET"], "plugins": { - "hmac-auth": {} + "hmac-auth": {} }, "upstream": { - "type": "roundrobin", - "nodes": { - "127.0.0.1:1980": 1 - } + "type": "roundrobin", + "nodes": { + "httpbin.org:80": 1 + } } -}' + }' ``` -## Example usage - -### Generating the signature +Generate a signature. You can use the below Python snippet or other stack of your choice: -The formula for calculating the signature is `signature = HMAC-SHAx-HEX(secret_key, signing_string)`. - -In order to generate the signature, two parameters, `secret_key` and `signing_string` are required. The `secret_key` is configured by a Consumer and the `signing_string` is calculated as `signing_string = HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key + \n + Date + \n + signed_headers_string`. The different terms in this calculation are explained below: - -- **HTTP Method** : HTTP request method in uppercase. For example, GET, PUT, POST etc. -- **HTTP URI** : HTTP URI. Should start with "/" and "/" denotes an empty path. -- **Date** : Date in the HTTP header in GMT format. -- **canonical_query_string** : The result of encoding the query string in the URL (the string "key1 = value1 & key2 = value2" after the "?" in the URL). -- **signed_headers_string** : Concatenation of the specified request headers. +```python title="hmac-sig-header-gen.py" +import hmac +import hashlib +import base64 +from datetime import datetime, timezone + +key_id = "john-key" # key id +secret_key = b"john-secret-key" # secret key +request_method = "GET" # HTTP method +request_path = "/get" # route URI +algorithm= "hmac-sha256" # can use other algorithms in allowed_algorithms + +# get current datetime in GMT +# note: the signature will become invalid after the clock skew (default 300s) +# you can regenerate the signature after it becomes invalid, or increase the clock +# skew to prolong the validity within the advised security boundary +gmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT') + +# construct the signing string (ordered) +# the date and any subsequent custom headers should be lowercased and separated by a +# single space character, i.e. `:` +# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6 +signing_string = ( + f"{key_id}\n" + f"{request_method} {request_path}\n" + f"date: {gmt_time}\n" +) + +# create signature +signature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest() +signature_base64 = base64.b64encode(signature).decode('utf-8') + +# construct the request headers +headers = { + "Date": gmt_time, + "Authorization": ( + f'Signature keyId="{key_id}",algorithm="{algorithm}",' + f'headers="@request-target date",' + f'signature="{signature_base64}"' + ) +} + +# print headers +print(headers) +``` -:::tip +Run the script: -If any of the terms are missing, they are replaced by an empty string. +```shell +python3 hmac-sig-header-gen.py +``` -::: +You should see the request headers printed: -**The algorithm for generating `canonical_query_string` is described below:** - -1. Extract the query terms from the URL. -2. Split the query terms into key-value pairs by using `&` as the separator. -3. If `encode_uri_params` is `true`: - 1. If there are only keys, the conversion formula is `uri_encode(key) + "="`. - 2. If there are both keys and values, the conversion formula is `uri_encode(key) + "=" + uri_encode(value)`. Here, the value can even be an empty string. - 3. Sort by key in lexicographic order and connect them with & symbol to generate the corresponding `canonical_query_string`. -4. If `encode_uri_params` is `false`: - 1. If there are only keys, the conversion formula is `key + "="`. - 2. If there are both keys and values, the conversion formula is `key + "=" + value`. Here, the value can even be an empty string. - 3. Sort by key in lexicographic order and connect them with & symbol to generate the corresponding `canonical_query_string`. - -And the algorithm for generating the `signed_headers_string` is as follows: - -1. Obtain the specified headers to add to the calculation from the request header. -2. Splice the specified headers in `name:value` format. This is the `signed_headers_string`. - -```plain -HeaderKey1 + ":" + HeaderValue1 + "\n"\+ -HeaderKey2 + ":" + HeaderValue2 + "\n"\+ -... -HeaderKeyN + ":" + HeaderValueN + "\n" +```text +{'Date': 'Fri, 06 Sep 2024 06:41:29 GMT', 'Authorization': 'Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM="'} ``` -The example below shows signature string splicing: +Using the headers generated, send a request to the route: ```shell -curl -i http://127.0.0.1:9080/index.html?name=james&age=36 \ --H "X-HMAC-SIGNED-HEADERS: User-Agent;x-custom-a" \ --H "x-custom-a: test" \ --H "User-Agent: curl/7.29.0" +curl -X GET "http://127.0.0.1:9080/get" \ + -H "Date: Fri, 06 Sep 2024 06:41:29 GMT" \ + -H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM="' ``` -### Explanation of signature generation formula process +You should see an `HTTP/1.1 200 OK` response similar to the following: -1. The default HTTP Method for the above request is GET, which gives `signing_string` as - -```plain -"GET" +```json +{ + "args": {}, + "headers": { + "Accept": "*/*", + "Authorization": "Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM=\"", + "Date": "Fri, 06 Sep 2024 06:41:29 GMT", + "Host": "127.0.0.1", + "User-Agent": "curl/8.6.0", + "X-Amzn-Trace-Id": "Root=1-66d96513-2e52d4f35c9b6a2772d667ea", + "X-Consumer-Username": "john", + "X-Credential-Identifier": "cred-john-hmac-auth", + "X-Forwarded-Host": "127.0.0.1" + }, + "origin": "192.168.65.1, 34.0.34.160", + "url": "http://127.0.0.1/get" +} ``` -2. The requested URI is `/index.html`, and the `signing_string` is obtained from the HTTP Method + \n + HTTP URI as - -```plain -"GET -/index.html" -``` +### Hide Authorization Information From Upstream -3. The query item in the URL is `name=james&age=36`, assuming that `encode_uri_params` is false. -According to the algorithm of `canonical_query_string`, the focus is on dictionary sorting of `key` to get `age=36&name=james`. +As seen the in the [last example](#implement-hmac-authentication-on-a-route), the `Authorization` header passed to the upstream includes the signature and all other details. This could potentially introduce security risks. -```plain -"GET -/index.html -age=36&name=james" -``` +The following example shows how to prevent these information from being sent to the upstream service. -4. The `access_key` is `user-key`, and the `signing_string` is obtained from HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key as +Update the plugin configuration to set `hide_credentials` to `true`: -```plain -"GET -/index.html -age=36&name=james -user-key" +```shell +curl "http://127.0.0.1:9180/apisix/admin/routes/hmac-auth-route" -X PATCH \ +-H "X-API-KEY: ${admin_key}" \ +-d '{ + "plugins": { + "hmac-auth": { + "hide_credentials": true + } + } +}' ``` -5. Date is in GMT format, as in `Tue, 19 Jan 2021 11:33:20 GMT`, and the `signing_string` is obtained from the HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key + \n + Date as +Send a request to the route: -```plain -"GET -/index.html -age=36&name=james -user-key -Tue, 19 Jan 2021 11:33:20 GMT" +```shell +curl -X GET "http://127.0.0.1:9080/get" \ + -H "Date: Fri, 06 Sep 2024 06:41:29 GMT" \ + -H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM="' ``` -6. `signed_headers_string` is used to specify the headers involved in the signature, which in the above example includes `User-Agent: curl/7.29.0` and `x-custom-a: test`. +You should see an `HTTP/1.1 200 OK` response and notice the `Authorization` header is entirely removed: -And the `signing_string` is obtained from the HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key + \n + Date + \n as - -```plain -"GET -/index.html -age=36&name=james -user-key -Tue, 19 Jan 2021 11:33:20 GMT -User-Agent:curl/7.29.0 -x-custom-a:test -" +```json +{ + "args": {}, + "headers": { + "Accept": "*/*", + "Host": "127.0.0.1", + "User-Agent": "curl/8.6.0", + "X-Amzn-Trace-Id": "Root=1-66d96513-2e52d4f35c9b6a2772d667ea", + "X-Consumer-Username": "john", + "X-Credential-Identifier": "cred-john-hmac-auth", + "X-Forwarded-Host": "127.0.0.1" + }, + "origin": "192.168.65.1, 34.0.34.160", + "url": "http://127.0.0.1/get" +} ``` -The Python code below shows how to generate the signature: - -```python -import base64 -import hashlib -import hmac +### Enable Body Validation -secret = bytes('my-secret-key', 'utf-8') -message = bytes("""GET -/index.html -age=36&name=james -user-key -Tue, 19 Jan 2021 11:33:20 GMT -User-Agent:curl/7.29.0 -x-custom-a:test -""", 'utf-8') +The following example shows how to enable body validation to ensure the integrity of the request body. -hash = hmac.new(secret, message, hashlib.sha256) +Create a route with the `hmac-auth` plugin as such: -# to lowercase base64 -print(base64.b64encode(hash.digest())) +```shell +curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -d '{ + "id": "hmac-auth-route", + "uri": "/post", + "methods": ["POST"], + "plugins": { + "hmac-auth": { + "validate_request_body": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "httpbin.org:80": 1 + } + } + }' ``` -| Type | Hash | -| --------- | -------------------------------------------- | -| SIGNATURE | 8XV1GB7Tq23OJcoz6wjqTs4ZLxr9DiLoY4PxzScWGYg= | - -You can also refer to [Generating HMAC signatures](../examples/plugins-hmac-auth-generate-signature.md) for how to generate signatures for different programming languages. +Generate a signature. You can use the below Python snippet or other stack of your choice: -### Validating request body - -When the `validate_request_body` attribute is set to `true`, the Plugin will calculate the HMAC-SHA value of the request body and checks it again the `X-HMAC-DIGEST` header: - -``` -X-HMAC-DIGEST: base64(hmac-sha()) +```python title="hmac-sig-digest-header-gen.py" +import hmac +import hashlib +import base64 +from datetime import datetime, timezone + +key_id = "john-key" # key id +secret_key = b"john-secret-key" # secret key +request_method = "POST" # HTTP method +request_path = "/post" # route URI +algorithm= "hmac-sha256" # can use other algorithms in allowed_algorithms +body = '{"name": "world"}' # example request body + +# get current datetime in GMT +# note: the signature will become invalid after the clock skew (default 300s). +# you can regenerate the signature after it becomes invalid, or increase the clock +# skew to prolong the validity within the advised security boundary +gmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT') + +# construct the signing string (ordered) +# the date and any subsequent custom headers should be lowercased and separated by a +# single space character, i.e. `:` +# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6 +signing_string = ( + f"{key_id}\n" + f"{request_method} {request_path}\n" + f"date: {gmt_time}\n" +) + +# create signature +signature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest() +signature_base64 = base64.b64encode(signature).decode('utf-8') + +# create the SHA-256 digest of the request body and base64 encode it +body_digest = hashlib.sha256(body.encode('utf-8')).digest() +body_digest_base64 = base64.b64encode(body_digest).decode('utf-8') + +# construct the request headers +headers = { + "Date": gmt_time, + "Digest": f"SHA-256={body_digest_base64}", + "Authorization": ( + f'Signature keyId="{key_id}",algorithm="hmac-sha256",' + f'headers="@request-target date",' + f'signature="{signature_base64}"' + ) +} + +# print headers +print(headers) ``` -If there is no request body, you can set the `X-HMAC-DIGEST` value to the HMAC-SHA of an empty string. +Run the script: -:::note - -To calculate the digest of the request body, the Plugin will load the body to memory which can cause high memory consumption if the body is large. To avoid this, you can limit the max allowed body size by configuring `max_req_body` (default 512KB). Request bodies larger than the set size will be rejected. +```shell +python3 hmac-sig-digest-header-gen.py +``` -::: +You should see the request headers printed: -### Using the generated signature to make requests +```text +{'Date': 'Fri, 06 Sep 2024 09:16:16 GMT', 'Digest': 'SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=', 'Authorization': 'Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE="'} +``` -You can now use the generated signature to make requests as shown below: +Using the headers generated, send a request to the route: ```shell -curl -i "http://127.0.0.1:9080/index.html?name=james&age=36" \ --H "X-HMAC-SIGNATURE: 8XV1GB7Tq23OJcoz6wjqTs4ZLxr9DiLoY4PxzScWGYg=" \ --H "X-HMAC-ALGORITHM: hmac-sha256" \ --H "X-HMAC-ACCESS-KEY: user-key" \ --H "Date: Tue, 19 Jan 2021 11:33:20 GMT" \ --H "X-HMAC-SIGNED-HEADERS: User-Agent;x-custom-a" \ --H "x-custom-a: test" \ --H "User-Agent: curl/7.29.0" +curl "http://127.0.0.1:9080/post" -X POST \ + -H "Date: Fri, 06 Sep 2024 09:16:16 GMT" \ + -H "Digest: SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=" \ + -H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE="' \ + -d '{"name": "world"}' ``` -```shell -HTTP/1.1 200 OK -Content-Type: text/html; charset=utf-8 -Transfer-Encoding: chunked -Connection: keep-alive -Date: Tue, 19 Jan 2021 11:33:20 GMT -Server: APISIX/2.2 -...... +You should see an `HTTP/1.1 200 OK` response similar to the following: + +```json +{ + "args": {}, + "data": "", + "files": {}, + "form": { + "{\"name\": \"world\"}": "" + }, + "headers": { + "Accept": "*/*", + "Authorization": "Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE=\"", + "Content-Length": "17", + "Content-Type": "application/x-www-form-urlencoded", + "Date": "Fri, 06 Sep 2024 09:16:16 GMT", + "Digest": "SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=", + "Host": "127.0.0.1", + "User-Agent": "curl/8.6.0", + "X-Amzn-Trace-Id": "Root=1-66d978c3-49f929ad5237da5340bbbeb4", + "X-Consumer-Username": "john", + "X-Credential-Identifier": "cred-john-hmac-auth", + "X-Forwarded-Host": "127.0.0.1" + }, + "json": null, + "origin": "192.168.65.1, 34.0.34.160", + "url": "http://127.0.0.1/post" +} ``` -The signature can be put in the `Authorization` header of the request: +If you send a request without the digest or with an invalid digest: ```shell -curl http://127.0.0.1:9080/index.html -H 'Authorization: hmac-auth-v1# + ACCESS_KEY + # + base64_encode(SIGNATURE) + # + ALGORITHM + # + DATE + # + SIGNED_HEADERS' -i +curl "http://127.0.0.1:9080/post" -X POST \ + -H "Date: Fri, 06 Sep 2024 09:16:16 GMT" \ + -H "Digest: SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=" \ + -H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE="' \ + -d '{"name": "world"}' ``` -```shell -HTTP/1.1 200 OK -Content-Type: text/html -Content-Length: 13175 -... -Accept-Ranges: bytes - - - -... +You should see an `HTTP/1.1 401 Unauthorized` response with the following message: + +```text +{"message":"client request can't be validated"} ``` -Or, the signature can be placed separately in another request header: +### Mandate Signed Headers -```shell -curl http://127.0.0.1:9080/index.html -H 'X-HMAC-SIGNATURE: base64_encode(SIGNATURE)' -H 'X-HMAC-ALGORITHM: ALGORITHM' -H 'Date: DATE' -H 'X-HMAC-ACCESS-KEY: ACCESS_KEY' -H 'X-HMAC-SIGNED-HEADERS: SIGNED_HEADERS' -i -``` +The following example shows how you can mandate certain headers to be signed in the request's HMAC signature. + +Create a route with the `hmac-auth` plugin which requires three headers to be present in the HMAC signature: ```shell -HTTP/1.1 200 OK -Content-Type: text/html -Content-Length: 13175 -... -Accept-Ranges: bytes - - - +curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -d '{ + "id": "hmac-auth-route", + "uri": "/get", + "methods": ["GET"], + "plugins": { + "hmac-auth": { + "signed_headers": ["date","x-custom-header-a", "x-custom-header-b"] + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "httpbin.org:80": 1 + } + } + }' ``` -:::note +Generate a signature. You can use the below Python snippet or other stack of your choice: -1. If there are multiple signed headers, they must be separated by `;`. For example, `x-custom-header-a;x-custom-header-b`. -2. `SIGNATURE` needs to be base64 encoded for encryption. +```python title="hmac-sig-req-header-gen.py" +import hmac +import hashlib +import base64 +from datetime import datetime, timezone + +key_id = "john-key" # key id +secret_key = b"john-secret-key" # secret key +request_method = "GET" # HTTP method +request_path = "/get" # route URI +algorithm= "hmac-sha256" # can use other algorithms in allowed_algorithms +custom_header_a = "hello123" # required custom header +custom_header_b = "world456" # required custom header + +# get current datetime in GMT +# note: the signature will become invalid after the clock skew (default 300s) +# you can regenerate the signature after it becomes invalid, or increase the clock +# skew to prolong the validity within the advised security boundary +gmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT') + +# construct the signing string (ordered) +# the date and any subsequent custom headers should be lowercased and separated by a +# single space character, i.e. `:` +# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6 +signing_string = ( + f"{key_id}\n" + f"{request_method} {request_path}\n" + f"date: {gmt_time}\n" + f"x-custom-header-a: {custom_header_a}\n" + f"x-custom-header-b: {custom_header_b}\n" +) + +# create signature +signature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest() +signature_base64 = base64.b64encode(signature).decode('utf-8') + +# construct the request headers +headers = { + "Date": gmt_time, + "Authorization": ( + f'Signature keyId="{key_id}",algorithm="hmac-sha256",' + f'headers="@request-target date x-custom-header-a x-custom-header-b",' + f'signature="{signature_base64}"' + ), + "x-custom-header-a": custom_header_a, + "x-custom-header-b": custom_header_b +} + +# print headers +print(headers) +``` -::: +Run the script: -### Using custom header keys +```shell +python3 hmac-sig-req-header-gen.py +``` -You can use custom header keys for the auth parameters by changing the `plugin_attr` in your configuration file (`conf/config.yaml`): +You should see the request headers printed: -```yaml -plugin_attr: - hmac-auth: - signature_key: X-APISIX-HMAC-SIGNATURE - algorithm_key: X-APISIX-HMAC-ALGORITHM - date_key: X-APISIX-DATE - access_key: X-APISIX-HMAC-ACCESS-KEY - signed_headers_key: X-APISIX-HMAC-SIGNED-HEADERS - body_digest_key: X-APISIX-HMAC-BODY-DIGEST +```text +{'Date': 'Fri, 06 Sep 2024 09:58:49 GMT', 'Authorization': 'Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date x-custom-header-a x-custom-header-b",signature="MwJR8JOhhRLIyaHlJ3Snbrf5hv0XwdeeRiijvX3A3yE="', 'x-custom-header-a': 'hello123', 'x-custom-header-b': 'world456'} ``` -Now you can use the new keys while making a request: +Using the headers generated, send a request to the route: ```shell -curl http://127.0.0.1:9080/index.html \ --H 'X-APISIX-HMAC-SIGNATURE: base64_encode(SIGNATURE)' \ --H 'X-APISIX-HMAC-ALGORITHM: ALGORITHM' \ --H 'X-APISIX-DATE: DATE' \ --H 'X-APISIX-HMAC-ACCESS-KEY: ACCESS_KEY' \ --H 'X-APISIX-HMAC-SIGNED-HEADERS: SIGNED_HEADERS' \ --H 'X-APISIX-HMAC-BODY-DIGEST: BODY_DIGEST' -i +curl -X GET "http://127.0.0.1:9080/get" \ + -H "Date: Fri, 06 Sep 2024 09:58:49 GMT" \ + -H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date x-custom-header-a x-custom-header-b",signature="MwJR8JOhhRLIyaHlJ3Snbrf5hv0XwdeeRiijvX3A3yE="' \ + -H "x-custom-header-a: hello123" \ + -H "x-custom-header-b: world456" ``` -```shell -HTTP/1.1 200 OK -Content-Type: text/html -Content-Length: 13175 -... -Accept-Ranges: bytes - - - +You should see an `HTTP/1.1 200 OK` response similar to the following: + +```json +{ + "args": {}, + "headers": { + "Accept": "*/*", + "Authorization": "Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"MwJR8JOhhRLIyaHlJ3Snbrf5hv0XwdeeRiijvX3A3yE=\"", + "Date": "Fri, 06 Sep 2024 09:58:49 GMT", + "Host": "127.0.0.1", + "User-Agent": "curl/8.6.0", + "X-Amzn-Trace-Id": "Root=1-66d98196-64a58db25ece71c077999ecd", + "X-Consumer-Username": "john", + "X-Credential-Identifier": "cred-john-hmac-auth", + "X-Custom-Header-A": "hello123", + "X-Custom-Header-B": "world456", + "X-Forwarded-Host": "127.0.0.1" + }, + "origin": "192.168.65.1, 103.97.2.206", + "url": "http://127.0.0.1/get" +} ``` ## Delete Plugin -To remove the `hmac-auth` Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect. +To remove the `hmac-auth` Plugin on the Route, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect. ```shell curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X PUT -d ' diff --git a/docs/zh/latest/plugins/hmac-auth.md b/docs/zh/latest/plugins/hmac-auth.md index a0eb74a78081..14264225baf7 100644 --- a/docs/zh/latest/plugins/hmac-auth.md +++ b/docs/zh/latest/plugins/hmac-auth.md @@ -30,29 +30,34 @@ description: 本文介绍了关于 Apache APISIX `hmac-auth` 插件的基本信 ## 描述 -`hmac-auth` 插件可以将 [HMAC authentication](https://en.wikipedia.org/wiki/HMAC) 添加到 Route 或者 Service。 +`hmac-auth` 插件支持 HMAC(基于哈希的消息认证码)身份验证,作为一种确保请求完整性的机制,防止它们在传输过程中被修改。要使用该插件,您需要在 [Consumers](../terminology/consumer.md) 上配置 HMAC 密钥,并在 Routes 或 Services 上启用该插件。 -该插件需要和 Consumer 一起使用,API 的使用者必须将密匙添加到请求头中以验证其请求。 +启用后,插件会验证请求的 `Authorization` 标头中的 HMAC 签名,并检查传入请求是否来自可信来源。具体来说,当 APISIX 收到 HMAC 签名的请求时,会从 `Authorization` 标头中提取密钥 ID。然后,APISIX 会检索相应的消费者配置,包括密钥。如果密钥 ID 有效且存在,APISIX 会使用请求的 `Date` 标头和密钥生成 HMAC 签名。如果生成的签名与 `Authorization` 标头中提供的签名匹配,则请求经过身份验证并转发到上游服务。 + +该插件实现基于 [draft-cavage-http-signatures](https://www.ietf.org/archive/id/draft-cavage-http-signatures-12.txt)。 ## 属性 +以下属性可用于 Consumers 或 Credentials 的配置。 + | 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | | ---------------- | ------------- | ------ | ------------- | ------------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| access_key | string | 是 | | | Consumer 的 `access_key` 必须是唯一的。如果不同 Consumer 使用了相同的 `access_key` ,将会出现请求匹配异常。 | -| secret_key | string | 是 | | | 与 `access_key` 配对使用。该字段支持使用 [APISIX Secret](../terminology/secret.md) 资源,将值保存在 Secret Manager 中。 | -| algorithm | string | 否 | "hmac-sha256" | ["hmac-sha1", "hmac-sha256", "hmac-sha512"] | 可以使用的加密算法。 | -| clock_skew | integer | 否 | 0 | | 签名允许的时间偏移(以秒为单位)。比如允许时间偏移 10 秒钟,那么就应设置为 `10`。如果将其设置为 `0`,则表示表示跳过日期检查。 | -| signed_headers | array[string] | 否 | | | 要在加密计算中使用的 headers 列表。指定后客户端请求只能在此范围内指定 headers,如果未指定,就会在所有客户端请求指定的 headers 加入加密计算。如: ["User-Agent", "Accept-Language", "x-custom-a"]。 | -| keep_headers | boolean | 否 | false | [ true, false ] | 当设置为 `true` 时,认证成功后的 HTTP 请求中则会保留 `X-HMAC-SIGNATURE`、`X-HMAC-ALGORITHM` 和 `X-HMAC-SIGNED-HEADERS` 的请求头。否则将移除 HTTP 请求头。 | -| encode_uri_params| boolean | 否 | true | [ true, false ] | 当设置为 `true` 时,对签名中的 URI 参数进行编码。例如:`params1=hello%2Cworld` 进行了编码,`params2=hello,world` 没有进行编码。设置为 `false` 时则不对签名中的 URI 参数编码。 | -| validate_request_body | boolean | 否 | false | [ true, false ] | 当设置为 `true` 时,对请求 body 做签名校验。 | -| max_req_body | integer | 否 | 512 * 1024 | | 最大允许的 body 大小。 | +| access_key | string | 是 | | | 消费者的唯一标识符,用于标识相关配置,例如密钥。 | +| secret_key | string | 是 | | | 用于生成 HMAC 的密钥。此字段支持使用 [APISIX Secret](../terminology/secret.md) 资源将值保存在 Secret Manager 中。 | -注意:schema 中还定义了 `encrypt_fields = {"secret_key"}`,这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。 +以下属性可用于 Routes 或 Services 的配置。 -## 启用插件 +| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | +| ---------------- | ------------- | ------ | ------------- | ------------------------------------------| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| allowed_algorithms | array[string] | 否 | ["hmac-sha1", "hmac-sha256", "hmac-sha512"] | "hmac-sha1"、"hmac-sha256" 和 "hmac-sha512" 的组合 | 允许的 HMAC 算法列表。 | +| clock_skew | integer | 否 | 300 | >=1 | 客户端请求的时间戳与 APISIX 服务器当前时间之间允许的最大时间差(以秒为单位)。这有助于解决客户端和服务器之间的时间同步差异,并防止重放攻击。时间戳将根据 Date 头中的时间(必须为 GMT 格式)进行计算。 | +| signed_headers | array[string] | 否 | | | 客户端请求的 HMAC 签名中应包含的 HMAC 签名头列表。 | +| validate_request_body | boolean | 否 | false | | 如果为 true,则验证请求正文的完整性,以确保在传输过程中没有被篡改。具体来说,插件会创建一个 SHA-256 的 base64 编码 digest,并将其与 `Digest` 头进行比较。如果 `Digest` 头丢失或 digest 不匹配,验证将失败。 | +| hide_credentials | boolean | 否 | false | | 如果为 true,则不会将授权请求头传递给上游服务。 | + +注意:schema 中还定义了 `encrypt_fields = {"secret_key"}`,这意味着该字段将会被加密存储在 etcd 中。具体参考 [加密存储字段](../plugin-develop.md#加密存储字段)。 -首先,我们需要在 Consumer 中启用该插件,如下所示: +## 例子 :::note @@ -64,336 +69,473 @@ admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"/ ::: +在继续之前,创建一个示例使用者并配置其凭据,该配置将用于下面的所有示例。 + +创建一个消费者 `john`: + ```shell -curl http://127.0.0.1:9180/apisix/admin/consumers \ --H "X-API-KEY: $admin_key" -X PUT -d ' -{ - "username": "jack", +curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -d '{ + "username": "john" + }' +``` + +创建 `john` 的 `hmac-auth` 凭证: + +```shell +curl "http://127.0.0.1:9180/apisix/admin/consumers/john/credentials" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -d '{ + "id": "cred-john-hmac-auth", "plugins": { - "hmac-auth": { - "access_key": "user-key", - "secret_key": "my-secret-key", - "clock_skew": 0, - "signed_headers": ["User-Agent", "Accept-Language", "x-custom-a"] - } + "hmac-auth": { + "key_id": "john-key", + "secret_key": "john-secret-key" + } } -}' + }' ``` - +以下示例展示了如何在路由上实现 HMAC 身份验证。 -然后就可以在 Route 或 Service 中启用插件,如下所示: +使用 `hmac-auth` 插件的默认配置创建路由: ```shell -curl http://127.0.0.1:9180/apisix/admin/routes/1 \ --H "X-API-KEY: $admin_key" -X PUT -d ' -{ - "uri": "/index.html", +curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -d '{ + "id": "hmac-auth-route", + "uri": "/get", + "methods": ["GET"], "plugins": { - "hmac-auth": {} + "hmac-auth": {} }, "upstream": { - "type": "roundrobin", - "nodes": { - "127.0.0.1:1980": 1 - } + "type": "roundrobin", + "nodes": { + "httpbin.org:80": 1 + } } -}' + }' ``` -## 签名算法详解 - -### 签名生成公式 - -在使用 `hmac-auth` 插件时,会涉及到签名。签名的计算公式为 `signature = HMAC-SHAx-HEX(secret_key, signing_string)`。 - -为了生成签名需要两个参数:`secret_key` 和 `signing_string`。其中 `secret_key` 由对应 Consumer 配置,`signing_string` 的计算公式为 `signing_string = HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key + \n + Date + \n + signed_headers_string`。以下是对计算公式中各个字段的释义: - -- **HTTP Method**:指 HTTP 协议中定义的 GET、PUT、POST 等请求方法,必须使用全大写的形式。 -- **HTTP URI**:HTTP URI。必须以 `/` 开头,`/` 表示空路径。 -- **Date**:请求头中的日期(GMT 格式)。 -- **canonical_query_string**:对 URL 中的 query(query 即 URL 中 `?` 后面的 `key1=valve1&key2=valve2` 字符串)进行编码后的结果。 -- **signed_headers_string**:从请求头中获取客户端指定的字段,并按顺序拼接字符串的结果。 - -:::tip 提示 - -- 如果 `signing_string` 中的任意一项不存在,则需要使用一个空字符串代替。 - -- 由于签名计算时,会区分大小写字母,在使用时,请规范其参数命名。 - -::: +生成签名。您可以使用以下 Python 代码片段或您选择的其他技术栈: -**生成 `canonical_query_string` 的算法描述如下:** - -1. 提取 URL 中的 query 项。 -2. 使用 `&` 作为分隔符,将 query 拆分成键值对。 -3. 如果 `encode_uri_params` 为 `true` 时: - - - 当该项有 `key` 时,转换公式为 `url_encode(key) + "="`。 - - 当该项同时有 `key` 和 `value` 时,转换公式为 `url_encode(key) + "=" + url_encode(value)` 。此处 `value` 可以是空字符串。 - - 将每一项转换后,以 `key` 按照字典顺序(ASCII 码由小到大)排序,并使用 `&` 符号连接起来,生成相应的 `canonical_query_string` 。 - -4. 如果 `encode_uri_params` 为 `false` 时: - - - 当该项只有 `key` 时,转换公式为 `key + "="` 。 - - 当该项同时有 `key` 和 `value` 时,转换公式为 `key + "=" + value` 。此处 `value` 可以是空字符串。 - - 将每一项转换后,以 `key` 按照字典顺序(ASCII 码由小到大)排序,并使用 `&` 符号连接起来,生成相应的 `canonical_query_string`。 - -**生成 `signed_headers_string` 的算法如下:** - -1. 从请求头中获取指定的 headers 加入计算中。 -2. 从请求头中按顺序取出 `SIGNED_HEADERS` 指定的 headers,并按顺序用 `name:value` 方式拼接起来,拼接完后就生成了 `signed_headers_string`。 - -```plain -HeaderKey1 + ":" + HeaderValue1 + "\n"\+ -HeaderKey2 + ":" + HeaderValue2 + "\n"\+ -... -HeaderKeyN + ":" + HeaderValueN + "\n" +```python title="hmac-sig-header-gen.py" +import hmac +import hashlib +import base64 +from datetime import datetime, timezone + +key_id = "john-key" # 密钥 ID +Secret_key = b"john-secret-key" # 秘密密钥 +request_method = "GET" # HTTP 方法 +request_path = "/get" # 路由 URI +algorithm= "hmac-sha256" # 可以在 allowed_algorithms 中使用其他算法 + +# 获取当前的 GMT 日期时间 +# 注意:时钟偏差后签名将失效(默认 300s) +# 签名失效后可以重新生成,或者增加时钟 +# 倾斜以延长建议的安全边界内的有效性 +gmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT') + +# 构造签名字符串(有序) +# 日期和任何后续的自定义标头应小写并用 +# 单空格字符,即 `:` +# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6 +signing_string = ( + f"{key_id}\n" + f"{request_method} {request_path}\n" + f"date: {gmt_time}\n" +) + +# 创建签名 +signature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest() +signature_base64 = base64.b64encode(signature).decode('utf-8') + +# 构造请求头 +headers = { + "Date": gmt_time, + "Authorization": ( + f'Signature keyId="{key_id}",algorithm="{algorithm}",' + f'headers="@request-target date",' + f'signature="{signature_base64}"' + ) +} + +# 打印请求头 +print(headers) ``` -### 签名生成公式过程详解 - -接下来,我们将以下述请求为例,为你介绍签名生成公式的具体计算过程: +运行脚本: ```shell -curl -i http://127.0.0.1:9080/index.html?name=james&age=36 \ --H "X-HMAC-SIGNED-HEADERS: User-Agent;x-custom-a" \ --H "x-custom-a: test" \ --H "User-Agent: curl/7.29.0" -``` - -1. 上文请求默认的 HTTP Method 是 GET,得到 `signing_string` 为: - -```plain -"GET" +python3 hmac-sig-header-gen.py ``` -2. 请求的 URI 是 `/index.html`,根据 HTTP Method + \n + HTTP URI 得到 `signing_string` 为: +您应该看到打印的请求标头: -```plain -"GET -/index.html" +```text +{'Date': 'Fri, 06 Sep 2024 06:41:29 GMT', 'Authorization': 'Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM="'} ``` -3. URL 中的 query 项是 `name=james&age=36`,假设 `encode_uri_params` 为 false,根据 `canonical_query_string` 的算法,重点是对 `key` 进行字典排序,得到 `age=36&name=james`;根据 HTTP Method + \n + HTTP URI + \n + canonical_query_string 得到 `signing_string` 为: +使用生成的标头,向路由发送请求: -```plain -"GET -/index.html -age=36&name=james" +```shell +curl -X GET "http://127.0.0.1:9080/get" \ + -H "Date: Fri, 06 Sep 2024 06:41:29 GMT" \ + -H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM="' ``` -4. access_key 是 `user-key`,根据 HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key 得到 `signing_string` 为: +您应该会看到类似于以下内容的 `HTTP/1.1 200 OK` 响应: -```plain -"GET -/index.html -age=36&name=james -user-key" +```json +{ + "args": {}, + "headers": { + "Accept": "*/*", + "Authorization": "Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM=\"", + "Date": "Fri, 06 Sep 2024 06:41:29 GMT", + "Host": "127.0.0.1", + "User-Agent": "curl/8.6.0", + "X-Amzn-Trace-Id": "Root=1-66d96513-2e52d4f35c9b6a2772d667ea", + "X-Consumer-Username": "john", + "X-Credential-Identifier": "cred-john-hmac-auth", + "X-Forwarded-Host": "127.0.0.1" + }, + "origin": "192.168.65.1, 34.0.34.160", + "url": "http://127.0.0.1/get" +} ``` -5. Date 是指 GMT 格式的日期,形如 `Tue, 19 Jan 2021 11:33:20 GMT`, 根据 HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key + \n + Date 得到 `signing_string` 为: +### 向上游隐藏授权信息 -```plain -"GET -/index.html -age=36&name=james -user-key -Tue, 19 Jan 2021 11:33:20 GMT" -``` +如 [上一个示例](#implement-hmac-authentication-on-a-route) 所示,传递给上游的 `Authorization` 标头包含签名和所有其他详细信息。这可能会带来安全风险。 -6. `signed_headers_string` 用来制定参与到签名的 headers,在上面示例中包括 `User-Agent: curl/7.29.0` 和 `x-custom-a: test`。 +以下示例展示了如何防止这些信息被发送到上游服务。 -根据 HTTP Method + \n + HTTP URI + \n + canonical_query_string + \n + access_key + \n + Date + \n + signed_headers_string + `\n`,得到完整的 `signing_string` 为: +更新插件配置以将 `hide_credentials` 设置为 `true`: -```plain -"GET -/index.html -age=36&name=james -user-key -Tue, 19 Jan 2021 11:33:20 GMT -User-Agent:curl/7.29.0 -x-custom-a:test -" +```shell +curl "http://127.0.0.1:9180/apisix/admin/routes/hmac-auth-route" -X PATCH \ +-H "X-API-KEY: ${admin_key}" \ +-d '{ + "plugins": { + "hmac-auth": { + "hide_credentials": true + } + } +}' ``` -### Body 校验 - -当 `validate_request_body` 设置为 `true` 时,插件将计算请求 body 的 `hmac-sha` 值,并与请求 headers 中的 `X-HMAC-DIGEST` 的值进行校验。 +发送以下请求: +```shell +curl -X GET "http://127.0.0.1:9080/get" \ + -H "Date: Fri, 06 Sep 2024 06:41:29 GMT" \ + -H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="wWfKQvPDr0wHQ4IHdluB4IzeNZcj0bGJs2wvoCOT5rM="' ``` -X-HMAC-DIGEST: base64(hmac-sha()) -``` - -如果没有请求 body,你可以将 `X-HMAC-DIGEST` 的值设置为空字符串的 HMAC-SHA。 - -:::note 注意 -当开启 body 校验时,为了计算请求 body 的 `hmac-sha` 值,该插件会把 body 加载到内存中,在请求 body 较大的情况下,可能会造成较高的内存消耗。 +您应该看到 `HTTP/1.1 200 OK` 响应,并注意到 `Authorization` 标头已被完全删除: -为了避免这种情况,你可以通过设置 `max_req_body`(默认值是 512KB)配置项来配置最大允许的 body 大小,body 超过此大小的请求会被拒绝。 +```json +{ + "args": {}, + "headers": { + "Accept": "*/*", + "Host": "127.0.0.1", + "User-Agent": "curl/8.6.0", + "X-Amzn-Trace-Id": "Root=1-66d96513-2e52d4f35c9b6a2772d667ea", + "X-Consumer-Username": "john", + "X-Credential-Identifier": "cred-john-hmac-auth", + "X-Forwarded-Host": "127.0.0.1" + }, + "origin": "192.168.65.1, 34.0.34.160", + "url": "http://127.0.0.1/get" +} +``` -::: +### 启用主体验证 -## 测试插件 +以下示例显示如何启用主体验证以确保请求主体的完整性。 -假设当前请求为: +使用 `hmac-auth` 插件创建路由,如下所示: ```shell -curl -i http://127.0.0.1:9080/index.html?name=james&age=36 \ --H "X-HMAC-SIGNED-HEADERS: User-Agent;x-custom-a" \ --H "x-custom-a: test" \ --H "User-Agent: curl/7.29.0" +curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -d '{ + "id": "hmac-auth-route", + "uri": "/post", + "methods": ["POST"], + "plugins": { + "hmac-auth": { + "validate_request_body": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "httpbin.org:80": 1 + } + } + }' ``` -通过以下 Python 代码为上述请求生成签名 `SIGNATURE`: +生成签名。您可以使用以下 Python 代码片段或您选择的其他技术栈: -```python -import base64 -import hashlib +```python title="hmac-sig-digest-header-gen.py" import hmac +import hashlib +import base64 +from datetime import datetime, timezone + +key_id = "john-key" # 密钥 ID +Secret_key = b"john-secret-key" # 秘密密钥 +request_method = "POST" # HTTP 方法 +request_path = "/post" # 路由 URI +algorithms= "hmac-sha256" # 可以在 allowed_algorithms 中使用其他算法 +body = '{"name": "world"}' # 请求正文示例 + +# 获取当前的 GMT 日期时间 +# 注意:时钟偏差(默认 300s)后签名将失效。 +# 签名失效后可以重新生成,或者增加时钟 +# 倾斜以延长建议的安全边界内的有效性 +gmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT') + +# 构造签名字符串(有序) +# 日期和任何后续的自定义标头应小写并用 +# 单空格字符,即 `:` +# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6 +signing_string = ( + f"{key_id}\n" + f"{request_method} {request_path}\n" + f"date: {gmt_time}\n" +) + +# 创建签名 +signature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest() +signature_base64 = base64.b64encode(signature).decode('utf-8') + +# 创建请求正文的 SHA-256 digest 并对其进行 Base64 编码 +body_digest = hashlib.sha256(body.encode('utf-8')).digest() +body_digest_base64 = base64.b64encode(body_digest).decode('utf-8') + +# 构造请求头 +headers = { + "Date": gmt_time, + "Digest": f"SHA-256={body_digest_base64}", + "Authorization": ( + f'Signature keyId="{key_id}",algorithm="hmac-sha256",' + f'headers="@request-target date",' + f'signature="{signature_base64}"' + ) +} + +# 打印请求头 +print(headers) +``` -secret = bytes('my-secret-key', 'utf-8') -message = bytes("""GET -/index.html -age=36&name=james -user-key -Tue, 19 Jan 2021 11:33:20 GMT -User-Agent:curl/7.29.0 -x-custom-a:test -""", 'utf-8') - -hash = hmac.new(secret, message, hashlib.sha256) +运行脚本: -# to lowercase base64 -print(base64.b64encode(hash.digest())) +```shell +python3 hmac-sig-digest-header-gen.py ``` -| Type | Hash | -| --------- | -------------------------------------------- | -| SIGNATURE | 8XV1GB7Tq23OJcoz6wjqTs4ZLxr9DiLoY4PxzScWGYg= | +您应该看到打印的请求标头: -你也可以参考 [Generating HMAC signatures](../../../en/latest/examples/plugins-hmac-auth-generate-signature.md) 了解如何使用不同的编程语言生成签名。 +```text +{'Date': 'Fri, 06 Sep 2024 09:16:16 GMT', 'Digest': 'SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=', 'Authorization': 'Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE="'} +``` -签名生成后,你可以通过以下示例使用生成的签名发起请求: +使用生成的标头,向路由发送请求: ```shell -curl -i "http://127.0.0.1:9080/index.html?name=james&age=36" \ --H "X-HMAC-SIGNATURE: 8XV1GB7Tq23OJcoz6wjqTs4ZLxr9DiLoY4PxzScWGYg=" \ --H "X-HMAC-ALGORITHM: hmac-sha256" \ --H "X-HMAC-ACCESS-KEY: user-key" \ --H "Date: Tue, 19 Jan 2021 11:33:20 GMT" \ --H "X-HMAC-SIGNED-HEADERS: User-Agent;x-custom-a" \ --H "x-custom-a: test" \ --H "User-Agent: curl/7.29.0" +curl "http://127.0.0.1:9080/post" -X POST \ + -H "Date: Fri, 06 Sep 2024 09:16:16 GMT" \ + -H "Digest: SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=" \ + -H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE="' \ + -d '{"name": "world"}' ``` -```shell -HTTP/1.1 200 OK -Content-Type: text/html; charset=utf-8 -Transfer-Encoding: chunked -Connection: keep-alive -Date: Tue, 19 Jan 2021 11:33:20 GMT -Server: APISIX/2.2 -...... +您应该会看到类似于以下内容的 `HTTP/1.1 200 OK` 响应: + +```json +{ + "args": {}, + "data": "", + "files": {}, + "form": { + "{\"name\": \"world\"}": "" + }, + "headers": { + "Accept": "*/*", + "Authorization": "Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date\",signature=\"rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE=\"", + "Content-Length": "17", + "Content-Type": "application/x-www-form-urlencoded", + "Date": "Fri, 06 Sep 2024 09:16:16 GMT", + "Digest": "SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=", + "Host": "127.0.0.1", + "User-Agent": "curl/8.6.0", + "X-Amzn-Trace-Id": "Root=1-66d978c3-49f929ad5237da5340bbbeb4", + "X-Consumer-Username": "john", + "X-Credential-Identifier": "cred-john-hmac-auth", + "X-Forwarded-Host": "127.0.0.1" + }, + "json": null, + "origin": "192.168.65.1, 34.0.34.160", + "url": "http://127.0.0.1/post" +} ``` -你也可以将签名放到请求头 `Authorization` 字段中: +如果您发送的请求没有 digest 或 digest 无效: ```shell -curl http://127.0.0.1:9080/index.html \ --H 'Authorization: hmac-auth-v1# + ACCESS_KEY + # + base64_encode(SIGNATURE) + # + ALGORITHM + # + DATE + # + SIGNED_HEADERS' -i +curl "http://127.0.0.1:9080/post" -X POST \ + -H "Date: Fri, 06 Sep 2024 09:16:16 GMT" \ + -H "Digest: SHA-256=78qzJuLwSpZ8HacsTdFCQJWxzPMOf8bYctRk2ySLpS8=" \ + -H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date",signature="rjS6NxOBKmzS8CZL05uLiAfE16hXdIpMD/L/HukOTYE="' \ + -d '{"name": "world"}' ``` -```shell -HTTP/1.1 200 OK -Content-Type: text/html -Content-Length: 13175 -... -Accept-Ranges: bytes - - - -... +您应该看到一个 `HTTP/1.1 401 Unauthorized` 响应,其中包含以下消息: + +```text +{"message":"client request can't be validated"} ``` -还可以将签名单独放在另一个请求头中: +### 强制签名标头 -```shell -curl http://127.0.0.1:9080/index.html \ --H 'X-HMAC-SIGNATURE: base64_encode(SIGNATURE)' \ --H 'X-HMAC-ALGORITHM: ALGORITHM' \ --H 'Date: DATE' \ --H 'X-HMAC-ACCESS-KEY: ACCESS_KEY' \ --H 'X-HMAC-SIGNED-HEADERS: SIGNED_HEADERS' -i -``` +以下示例展示了如何强制在请求的 HMAC 签名中对某些标头进行签名。 + +使用 `hmac-auth` 插件创建路由,该路由要求 HMAC 签名中存在三个标头: ```shell -HTTP/1.1 200 OK -Content-Type: text/html -Content-Length: 13175 -... -Accept-Ranges: bytes - - - +curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -d '{ + "id": "hmac-auth-route", + "uri": "/get", + "methods": ["GET"], + "plugins": { + "hmac-auth": { + "signed_headers": ["date","x-custom-header-a", "x-custom-header-b"] + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "httpbin.org:80": 1 + } + } + }' ``` -:::note 注意 +生成签名。您可以使用以下 Python 代码片段或您选择的其他技术栈: -1. ACCESS_KEY、SIGNATURE、ALGORITHM、DATE、SIGNED_HEADERS 分别代表对应的变量。 -2. SIGNED_HEADERS 为客户端指定的加入加密计算的 headers。若存在多个 headers 需以 `;` 分割,例如:`x-custom-header-a;x-custom-header-b`。 -3. SIGNATURE 需要使用 base64 进行加密:`base64_encode(SIGNATURE)`。 +```python title="hmac-sig-req-header-gen.py" +import hmac +import hashlib +import base64 +from datetime import datetime, timezone + +key_id = "john-key" # 密钥 ID +Secret_key = b"john-secret-key" # 秘密密钥 +request_method = "GET" # HTTP 方法 +request_path = "/get" # 路由 URI +algorithms= "hmac-sha256" # 可以在 allowed_algorithms 中使用其他算法 +custom_header_a = "hello123" # 必需的自定义标头 +custom_header_b = "world456" # 必需的自定义标头 + +# 获取当前的 GMT 日期时间 +# 注意:时钟偏差后签名将失效(默认 300s) +# 签名失效后可以重新生成,或者增加时钟 +# 倾斜以延长建议的安全边界内的有效性 +gmt_time = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT') + +# 构造签名字符串(有序) +# 日期和任何后续的自定义标头应小写并用 +# 单空格字符,即 `:` +# https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-2.1.6 +signing_string = ( + f"{key_id}\n" + f"{request_method} {request_path}\n" + f"date: {gmt_time}\n" + f"x-custom-header-a: {custom_header_a}\n" + f"x-custom-header-b: {custom_header_b}\n" +) + +# 创建签名 +signature = hmac.new(secret_key, signing_string.encode('utf-8'), hashlib.sha256).digest() +signature_base64 = base64.b64encode(signature).decode('utf-8') + +# 构造请求头 +headers = { + "Date": gmt_time, + "Authorization": ( + f'Signature keyId="{key_id}",algorithm="hmac-sha256",' + f'headers="@request-target date x-custom-header-a x-custom-header-b",' + f'signature="{signature_base64}"' + ), + "x-custom-header-a": custom_header_a, + "x-custom-header-b": custom_header_b +} + +# 打印请求头 +print(headers) +``` -::: +运行脚本: -### 自定义 header 名称 +```shell +python3 hmac-sig-req-header-gen.py +``` -除了配置签名外,你还可以在配置文件(`conf/config.yaml`)中的 `plugin_attr` 配置项下,添加 `hmac-auth` 插件的属性来自定义参数 header 名称。如下所示: +您应该看到打印的请求标头: -```yaml title="conf/config.yaml" -plugin_attr: - hmac-auth: - signature_key: X-APISIX-HMAC-SIGNATURE - algorithm_key: X-APISIX-HMAC-ALGORITHM - date_key: X-APISIX-DATE - access_key: X-APISIX-HMAC-ACCESS-KEY - signed_headers_key: X-APISIX-HMAC-SIGNED-HEADERS - body_digest_key: X-APISIX-HMAC-BODY-DIGEST +```text +{'Date': 'Fri, 06 Sep 2024 09:58:49 GMT', 'Authorization': 'Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date x-custom-header-a x-custom-header-b",signature="MwJR8JOhhRLIyaHlJ3Snbrf5hv0XwdeeRiijvX3A3yE="', 'x-custom-header-a': 'hello123', 'x-custom-header-b': 'world456'} ``` -配置完成后,你可以使用自定义的 header 发起请求。 +使用生成的标头,向路由发送请求: ```shell -curl http://127.0.0.1:9080/index.html \ --H 'X-APISIX-HMAC-SIGNATURE: base64_encode(SIGNATURE)' \ --H 'X-APISIX-HMAC-ALGORITHM: ALGORITHM' \ --H 'X-APISIX-DATE: DATE' \ --H 'X-APISIX-HMAC-ACCESS-KEY: ACCESS_KEY' \ --H 'X-APISIX-HMAC-SIGNED-HEADERS: SIGNED_HEADERS' \ --H 'X-APISIX-HMAC-BODY-DIGEST: BODY_DIGEST' -i +curl -X GET "http://127.0.0.1:9080/get" \ + -H "Date: Fri, 06 Sep 2024 09:58:49 GMT" \ + -H 'Authorization: Signature keyId="john-key",algorithm="hmac-sha256",headers="@request-target date x-custom-header-a x-custom-header-b",signature="MwJR8JOhhRLIyaHlJ3Snbrf5hv0XwdeeRiijvX3A3yE="' \ + -H "x-custom-header-a: hello123" \ + -H "x-custom-header-b: world456" ``` -``` -HTTP/1.1 200 OK -Content-Type: text/html -Content-Length: 13175 -... -Accept-Ranges: bytes - - - +您应该会看到类似于以下内容的 `HTTP/1.1 200 OK` 响应: + +```json +{ + "args": {}, + "headers": { + "Accept": "*/*", + "Authorization": "Signature keyId=\"john-key\",algorithm=\"hmac-sha256\",headers=\"@request-target date x-custom-header-a x-custom-header-b\",signature=\"MwJR8JOhhRLIyaHlJ3Snbrf5hv0XwdeeRiijvX3A3yE=\"", + "Date": "Fri, 06 Sep 2024 09:58:49 GMT", + "Host": "127.0.0.1", + "User-Agent": "curl/8.6.0", + "X-Amzn-Trace-Id": "Root=1-66d98196-64a58db25ece71c077999ecd", + "X-Consumer-Username": "john", + "X-Credential-Identifier": "cred-john-hmac-auth", + "X-Custom-Header-A": "hello123", + "X-Custom-Header-B": "world456", + "X-Forwarded-Host": "127.0.0.1" + }, + "origin": "192.168.65.1, 103.97.2.206", + "url": "http://127.0.0.1/get" +} ``` ## 删除插件 -当你需要删除该插件时,可以通过以下命令删除相应的 JSON 配置,APISIX 将会自动重新加载相关配置,无需重启服务: +当你需要在路由上删除该插件时,可以通过以下命令删除相应的 JSON 配置,APISIX 将会自动重新加载相关配置,无需重启服务: ```shell curl http://127.0.0.1:9180/apisix/admin/routes/1 \