Jenkins Pipeline生成AWS4-HMAC-SHA256签名时匹配失败问题排查
问题:Jenkins Pipeline生成AWS V4签名访问Scaleway S3桶时签名不匹配
我尝试在Jenkins Pipeline中生成AWS V4认证签名以访问Scaleway的S3存储桶,但执行后收到「签名不匹配」的响应。已排查:
- Postman使用相同凭证可正常访问目标桶;
- SHA256Hash和sign函数生成的哈希值与在线工具结果一致;
- 已对照Scaleway官方文档验证格式、换行符等规范。
以下是我的Pipeline脚本:
import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import org.apache.commons.codec.binary.Hex; def SHA256Hash(String input) { // Create a MessageDigest object for SHA-256 hashing def digest = MessageDigest.getInstance('SHA-256') // Convert the input string to bytes and hash it def hashedBytes = digest.digest(input.getBytes('UTF-8')) // Convert the hashed bytes to a hexadecimal string def hexString = new StringBuilder() hashedBytes.each { b -> def hex = Integer.toHexString(0xff & b) if (hex.length() == 1) hexString.append('0') hexString.append(hex) } return hexString.toString() } def sign(key, msg) { SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(keySpec); byte[] signatureBytes = mac.doFinal(msg.getBytes(StandardCharsets.UTF_8)); echo "thats from function ${signatureBytes}" return Hex.encodeHexString(signatureBytes); } pipeline { agent any environment { AWS_ACCESS_KEY_ID = credentials('my-bucket-api-access-key') AWS_SECRET_ACCESS_KEY = credentials('my-bucket-api-secret-key') AWS_DEFAULT_REGION = 'fr-par' SERVICE = 's3' S3_BUCKET_NAME = "NAME_OF_BUCKET" HOST = "${S3_BUCKET_NAME}.s3.${AWS_DEFAULT_REGION}.scw.cloud" } stages { stage('AWS Version 4 signing') { steps { script { def date_stamp = new Date().format("yyyyMMdd", TimeZone.getTimeZone("UTC")) //generate signing key def signing_key = "AWS4${AWS_SECRET_ACCESS_KEY}" signing_key =sign(sign(sign(sign(signing_key, date_stamp), AWS_DEFAULT_REGION), SERVICE), 'aws4_request') echo " signing_key ${signing_key}" //generate string to sign def method = 'GET' def amz_date = new Date().format("yyyyMMdd'T'HHmmss'Z'", TimeZone.getTimeZone("UTC")) def canonical_uri = '/' def canonical_querystring = '' def Amz_Content_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" def payload_hash = Amz_Content_SHA256 def canonical_headers = "host:${HOST}\nx-amz-content-sha256:${Amz_Content_SHA256}\nx-amz-date:${amz_date}" def signed_headers = 'host;x-amz-content-sha256;x-amz-date' def canonical_request = "${method}\n${canonical_uri}\n${canonical_querystring}\n${canonical_headers}\n${signed_headers}\n${payload_hash}" echo " canonical_request ${canonical_request}" def algorithm = 'AWS4-HMAC-SHA256' def credential_scope = "${date_stamp}/${AWS_DEFAULT_REGION}/${SERVICE}/aws4_request" def canonicalRequestHash = SHA256Hash(canonical_request) def string_to_sign = "${algorithm}\n${amz_date}\n${credential_scope}\n${canonicalRequestHash}" echo " string_to_sign ${string_to_sign}" //create signature def signature = sign(signing_key, string_to_sign) echo " signature ${signature}" //create authorization header def authorization_header = "${algorithm} Credential=${AWS_ACCESS_KEY_ID}/${credential_scope},SignedHeaders=${signed_headers},Signature=${signature}" echo " authorization_header ${authorization_header}" def s3ListObjectsUrl = "http://${S3_BUCKET_NAME}.s3.${AWS_DEFAULT_REGION}.scw.cloud" def s3Host = "${S3_BUCKET_NAME}.s3.${AWS_DEFAULT_REGION}.scw.cloud" sh """ curl -X GET "${s3ListObjectsUrl}" \ -H "Host: ${s3Host}" \ -H "X-Amz-Content-Sha256: ${Amz_Content_SHA256}" \ -H "X-Amz-Date: ${amz_date}" \ -H "Authorization: ${authorization_header}" """ } } } } }
解决方案
1. 核心错误:签名密钥生成流程错误
AWS V4签名要求每一步HMAC操作都基于前一步的原始字节数组,而非其十六进制字符串表示。你的sign函数返回的是HMAC结果的十六进制字符串,后续调用时将该字符串作为密钥传入,相当于把十六进制字符的字节值作为密钥,而非原始HMAC字节数组,导致后续签名计算完全错误。
修正方式:
修改sign函数返回字节数组,调整签名密钥生成逻辑:
// 重新定义sign函数,返回字节数组而非十六进制字符串 def sign(byte[] key, String msg) { SecretKeySpec keySpec = new SecretKeySpec(key, "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(keySpec); return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8)); } // 生成签名密钥的正确流程 def signing_key = ("AWS4${AWS_SECRET_ACCESS_KEY}").getBytes(StandardCharsets.UTF_8) signing_key = sign(signing_key, date_stamp) signing_key = sign(signing_key, AWS_DEFAULT_REGION) signing_key = sign(signing_key, SERVICE) signing_key = sign(signing_key, 'aws4_request') // 生成最终签名时,再将字节数组转十六进制 def signature = Hex.encodeHexString(sign(signing_key, string_to_sign))
2. 统一时间戳生成,避免时间偏差
两次调用new Date()可能导致date_stamp和amz_date的时间不一致(比如跨秒),统一用同一个时间对象更严谨:
def now = new Date(TimeZone.getTimeZone("UTC")) def date_stamp = now.format("yyyyMMdd") def amz_date = now.format("yyyyMMdd'T'HHmmss'Z'")
3. 使用HTTPS请求Scaleway S3端点
Scaleway S3默认要求HTTPS访问,你的脚本用HTTP可能导致签名验证失败(Postman大概率使用的是HTTPS):
def s3ListObjectsUrl = "https://${S3_BUCKET_NAME}.s3.${AWS_DEFAULT_REGION}.scw.cloud"
4. 避免重复发送Host头
curl请求URL中已包含主机信息,手动添加-H "Host: ${s3Host}"可能导致重复Host头,可移除该参数,让curl自动生成正确的Host头。
内容的提问来源于stack exchange,提问作者selmin
相关产品推荐
相关产品推荐

