S3对象存储AWS4签名分析与实现
1. 前言
在用sdk跟rgw交互的时候,比如python下常用的boto3和boto或者s3cmd 工具,时常会遇到如下的问题
SignatureDoesNotMatch
直译过来就是 签名不匹配,所以到底这个签名是什么,他是怎么个签名的过程?
2. AWS 签名的种类
我们知道S3对象存储服务是由亚马逊发扬光大的,亚马逊也定义S3的整个标准,比如ceph 的rgw,minio的s3服务都是按照亚马逊的标准来开发的,所以,如果你想找关于S3标准的文档比如签名文档,API接口文档,那么你直接去AWS官网一定可以找到最完整的文档(划重点),需要注意的事,AWS s3拥有的特性,ceph rgw是不一定有,这个还需要参考ceph官网。
2.1 什么是aws 签名
s3服务是通过http协议提供服务的,流量在网络上流通是很容易被拦截,伪造的,修改的,签名是一种加密过程,签名有几个目的
- 避免明文传输秘钥
- 防止内容篡改
- 请求的有效性(是否过期)
2.2 AWS 2 和 AWS 4 签名
AWS 2 签名是早期的签名方式,签名过程简单,但是亚马逊已经不推荐使用,后期也将不支持这个签名方式,但是ceph rgw是有实现的,所以这里就不再分析了,把重心放在AWS 4上。
AWS 4版本的签名过程是一个比较繁琐的过程,规矩很多,所以需要非常细心的去实现。
3. AWS 4签名过程
首先来看一个S3 获取对象的GET OBJECT 接口的http 请求,这是一个可以正常获取对象的请求
GET /data_bucket/usysysysysys HTTP/1.1
Host: 172.26.2.41:8000
User-Agent: Go-http-client/1.1
Authorization: AWS4-HMAC-SHA256 Credential=admin/20190830/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=ab33ca157e8e962b5bc9186fc7b30bba257d56128c603f28d4259fa71989a2fe
X-Amz-Content-Sha256: UNSIGNED-PAYLOAD
X-Amz-Date: 20190830T085636Z
Accept-Encoding: gzip
这里需要特别注意的是,AWS 的每一个API接口对header的要求是不一样,这里header简单分为三种
所有接口都需要的header
比如 host, X-Amz-Content-Sha256,X-Amz-Date (或者Date) 是所有的接口都需要
对应接口必须要有的头部
有些接口需要特定的header, 比如 x-amz-storage-class 是单块上传接口必须要设置的
可选的头部
当然这些是对应的API开发问题,我们先专注与签名这一块,签名是一个公共模块,对所有的接口都是一样的
AWS 的签名过程,无非是对,http header 、http body 和用户的秘钥, 按照一定的规则进行哈希,得到惟一值 signature ,服务端校验的过程,并不是一般意义上的解密过程,而是服务器对http的内容进行再一次的计算,如果计算的结果跟客户端计算的签名是一样的,则表明请求合法,总体的过程如下
aws4 的签名过程在官方文档中有非常详细的说明,所以我这里不会很具体的分析每一步,但是会将遇到的需要特别注意的点提取出来,少踩坑。
首先还是看一张整体的签名思维导图 (图片太小右键其他标签页打开)
3.1 URI编码和哈希算法
AWS4 签名过程中大量使用URI编码和两种哈希算法,这URI编码和两种哈希算法对应的开发语言一般都会有现成的实现
sha256
aws要求返回的结果必须是小写的16进制编码的结果
Hmac
一种加’盐’的哈希算法, 内部使用的是sha256哈希
URI 编码
因为uri中可能出现各种各样的特殊符号或者中文,所以在传输前需要对uri进行编码, 关于URL编码
3.2 签名第一步: 创建规范请求
这一步是签名过程中最琐碎规则最多的一个部分,整个过程包含6个部分的内容处理,每一部分处理完成后按照如下的规则拼接成一个字符串
CanonicalRequest = HTTPRequestMethod + '\n' + CanonicalURI + '\n' + CanonicalQueryString + '\n' + CanonicalHeaders + '\n' + SignedHeaders + '\n' + HexEncode(Hash(RequestPayload))
CanonicalURI
URI 需要进行uri编码,具体要求看官网
这一部分是对整个请求url的uri部分提取出来,进行uri编码,如果uri是空的,用一个 ‘/’ 斜杠代替
CanonicalQueryString
这一部分是对url中的参数部分进行处理,注意这一步也很重要,这里对参数名需要进行排序,排序的依据是,根据ASCII码对应的值由小到大排序,可以找ASCII 表看看,对参数名和值进行URI编码, 更具体的要求找官网查看
CanonicalHeaders
对HTTP 头部处理,注意了,这里头部字段不管是大写,小写,还是大小写混合,都需要这样一个处理过程
- 将头部字段转换为小写
- 转换为小写后排序
- 对头部值去除前后空格
- 如果头部值字符串中间出现多个空格(2个或者以上)的,用一个空格代替
SignedHeaders
刚刚提到,header的是否参与到签名中是有一些规定的,有些header必须要参与到签名中,有些可以参与也可以不参与,那到底服务器怎么知道哪些参与了签名呢?答案就在这里,这一部分就是要记录参与了签名的头部字段。
HexEncode(Hash(RequestPayload))
这一步是对body 进行 sha256 编码,这是一个可选项,aws并不强制要求一定要对body哈希,毕竟body可能会很大,hash时间会很长。
Signed payload option You include the payload hash when constructing the canonical request (that then becomes part of StringToSign, as explained in the signature calculation section). You also specify the same value as the x-amz-content-sha256 header value when sending the request to S3. Unsigned payload option You include the literal string UNSIGNED-PAYLOAD when constructing a canonical request, and set the same value as the x-amz-content-sha256 header value when sending the request to S3. When you send your request to S3, the x-amz-content-sha256 header value informs S3 whether the payload is signed or not. Amazon S3 can then create signature accordingly for verification
处理之后的结果类似如下格式
GET / Action=ListUsers&Version=2010-05-08 content-type:application/x-www-form-urlencoded; charset=utf-8 host:iam.amazonaws.com x-amz-date:20150830T123600Z content-type;host;x-amz-date e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b85
最后,对拼接成的字符串进行sha256 哈希。
3.2 签名第二步: 创建待签字符串
待签字符串是一个如下的字符串
AWS4-HMAC-SHA256 20150830T123600Z 20150830/us-east-1/iam/aws4_request f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59
AWS4-HMAC-SHA256
算法的名字,一般也就这个值,代表用什么类型的哈希算法
20150830T123600Z
这是一个 ISO8601 格式的日期时间格式 YYYYMMDD’T’HHMMSS’Z’
特别注意的是,整个签名到http请求发送的过程中时间值都应该是同一个
20150830/us-east-1/iam/aws4_request
格式为 日期/区域/iam/aws4_request
最后一部分是 签名第一步的哈希值
3.3 签名第三步: 计算签名
秘钥哈希的伪代码如下所示
kSecret = your secret access key // s3 用户的secret key kDate = HMAC("AWS4" + kSecret, Date) kRegion = HMAC(kDate, Region) kService = HMAC(kRegion, Service) kSigning = HMAC(kService, "aws4_request")
这里多次的加‘盐’Hmac 哈希,可以看到,这里用户的秘钥参与到了哈希中,得到一个秘钥的哈希结果 singingKey ,注意,这里每一步hmac的结果值都会参与到下一个hamc中
最后将 signKey 跟 签名第二部的待签字符串进行 hmac
signature = HexEncode(HMAC(signKey, string to sign))
到此计算签名的过程就结束了。
3.4 最后一步: 添加Http 头部 Authorization
Authorization: algorithm Credential=access key ID/credential scope, SignedHeaders=SignedHeaders, Signature=signature
SignedHeaders 就是创建规范签名中的参与了签名的头部字段
示例如下
Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7
这里借用官方的签名流程图总结一下整个签名过程
所以出现签名不匹配的原因无外乎这几个
- 签名的版本不对
- 客户端签名过程中不对,所以导致跟服务器签名结果不一致
- 经过中间层代理之后出现不匹配,中间层代理修改了参与到签名的头部字段信息,比如nginx 可能会出现修改host的操作。
最后贴出笔者实现的aws4 签名的go语言实现版Demo, github传送门