HMAC认证

HMAC是一种常见的保护API技术手段。比如AWS的API就都是HMAC认证的。HMAC采用一种对称算法签名,请求发起方(客户端)和服务方(服务器端)使用共同的加密算法,比如SHA256,比如SM4等;在签名的时候,采用共享的加密因子(secret)。客户端在生成签名的时候,可以选择使用哪些HTTP Header;然后把签名的结果放在特定的HTTP Header里边,比如标准的Authorization。服务器端收到请求后,用相同的算法和加密因子来计算签名,如果和客户端发送的结果相同,那么就通过验证。

通常,请求的信息里也包含请求方的用户名,用username参数,所以签名通过后,可以确认请求所声明的用户是可信的。

另外,HMAC通常会对HTTP Header里的时间信息做签名,这些信息一般放在Date或者X-Date的Header里边。服务器端在签名验证通过后,可以选择性的对这个时间戳进行校验,比如双方约定请求从客户端发起到服务器端不能超过10秒钟,那么服务器就可以比对这个客户端发来的时间和系统时间,如果超出时间,也拦截这个请求。

除了用于签名验证外,HMAC通常可以用于防止回放攻击(http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.69.1965&rep=rep1&type=pdf)。

这里有一个不错的文章,介绍了HMAC:https://www.wolfe.id.au/2012/10/20/what-is-hmac-authentication-and-why-is-it-useful/

创建一个API

首先我们创建一个API,具体的操作过程可以参考“101反向代理”。

创建好的API如下图: HMAC演示

如果没有自己的环境用于这实验,可以用我们的在线测试环境:http://demo.flomesh.cn 或者 http://demo2.flomesh.cn ,登录用户密码都是admin。在demo2的环境里,可以看到我做的这个例子。

设定服务端HMAC认证

在API的编辑界面,点击中间的策略编辑区,然后在“认证”的选项卡里选择“HMAC”。除了Clock Skew之外,其他参数都用默认值即可;为了测试目的,Clock Skew可以设置一个很大的值,比如300000,这个参数的单位是“秒”,描述的是:客户端时间戳和服务器校验之间的时间不能大于这个值,否则请求无效。

设置的结果如图所示: HMAC设定

保存后的结果这样: HMAC设置

添加访问用户

部署这个API之后,这个时候如果访问这个API,会看到401错误:

caishu@dev$ curl -i -H "Host: hmacdemo.flomesh.cn" http://54.249.57.49:30001/1.html 
HTTP/1.1 401 Unauthorized
Date: Wed, 04 Dec 2019 05:25:35 GMT
Content-Type: application/json; charset=utf-8
Connection: keep-alive
Content-Length: 26
Server: kong/1.2.2

{"message":"Unauthorized"}

我们在发送请求的时候需要携带HMAC的签名信息,这里的部分信息需要首先配置到Flomesh里边。首先我们添加一个“客户端”,在Flomesh里边,我们用“应用”来管理客户端。点击左侧菜单“应用”,创建一个叫做“HMAC客户端”的应用,在“身份标识”的标签页里,我们添加一个用户,如下图: 添加访问身份信息

订阅

接下来我们需要把这个“应用”订阅到API上。应用和API之间的订阅关系,确定了哪些应用可以访问哪些API;没有订阅的话,即使用户凭证正确,也无法访问。

用这个例子来理解订阅:API设定了访问控制,就像给一个房间加了一个“刷卡开门锁”;添加一个应用和身份信息,等于是发出一个“用工卡”;订阅就是给这个员工卡授权可以打开这个锁。

在“HMAC客户端”这个应用里选择“订阅”标签,然后添加一个订阅: 应用订阅API

完成订阅之后,需要部署这个订阅,点击这个订阅,然后选择“提交”,然后选择“部署”,部署成功的状态是“授权”: 部署订阅

这个时间就可以用客户端发起含有HMAC信息的请求来验证这个配置了。

验证

我们采用如下的这个shell脚本来生成HMAC签名,并发起请求:

export LC_ALL=en_US.UTF8
DATE=`date -u +"%a, %d %b %Y %T GMT"`
SIGNING_STRING="X-Date: $DATE"
SECRET='root123'
DIGEST=$(echo -n "$SIGNING_STRING" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)
echo "DIGEST is '$DIGEST'"

echo ""
echo "开始测试===>"
echo ""

curl -i -vvvv http://54.249.57.49:30001/1.html \
    -H "$SIGNING_STRING" \
    -H "Host: hmacdemo.flomesh.cn" \
    -H "Authorization: hmac username=\"caishu\", algorithm=\"hmac-sha256\", headers=\"X-Date\", signature=\"$DIGEST\""

注意这里的日期格式,只有这个格式才是可以通过验证的;另外,签名的时候,我们用了openssl工具,如果没有的话,需要自己安装,不过多数linux都是包含这个工具的。

我们把这个脚本保存成test-hmac.sh,然后执行就能看到测试结果:

caishu@dev$ bash test-hmac.sh 
DIGEST is 'FFPBZSrQrXu2WWSlejuyU4/by8by+O1VgoOYBxUNyp8='

开始测试===>

*   Trying 54.249.57.49...
* TCP_NODELAY set
* Connected to 54.249.57.49 (54.249.57.49) port 30001 (#0)
> GET /1.html HTTP/1.1
> Host: hmacdemo.flomesh.cn
> User-Agent: curl/7.52.1
> Accept: */*
> X-Date: Wed, 04 Dec 2019 05:56:58 GMT
> Authorization: hmac username="caishu", algorithm="hmac-sha256", headers="X-Date", signature="FFPBZSrQrXu2WWSlejuyU4/by8by+O1VgoOYBxUNyp8="
> 
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Content-Type: text/html; charset=UTF-8
Content-Type: text/html; charset=UTF-8
< Content-Length: 13
Content-Length: 13
< Connection: keep-alive
Connection: keep-alive
< Server: nginx/1.14.2
Server: nginx/1.14.2
< Date: Wed, 04 Dec 2019 05:56:58 GMT
Date: Wed, 04 Dec 2019 05:56:58 GMT
< Last-Modified: Mon, 02 Dec 2019 15:26:18 GMT
Last-Modified: Mon, 02 Dec 2019 15:26:18 GMT
< ETag: "5de52d1a-d"
ETag: "5de52d1a-d"
< Accept-Ranges: bytes
Accept-Ranges: bytes
< X-Kong-Upstream-Latency: 7
X-Kong-Upstream-Latency: 7
< X-Kong-Proxy-Latency: 7
X-Kong-Proxy-Latency: 7
< Via: kong/1.2.2
Via: kong/1.2.2

< 
13.113.5.188
* Curl_http_done: called premature == 0
* Connection #0 to host 54.249.57.49 left intact

注意这里我做了设置:export LC_ALL=en_US.UTF8。因为在一些中文的term环境里,生成的日期信息是包含中文字符的,而网关不识别这些中文字符。