Flutter中如何使用ES256算法及私钥签名字符串?
解决Apple Web Snapshots API的ES256签名问题(Flutter)
你遇到的401错误根源很明确:HmacSHA256是哈希消息认证码算法,而Apple要求的ES256是基于P-256曲线的ECDSA(椭圆曲线数字签名算法),两者完全不是同一类算法,用Hmac生成的签名自然无法通过Apple的身份验证。
Flutter自带的crypto库只提供基础哈希功能,不支持ECDSA签名,我们需要借助pointycastle库来实现ES256签名,以下是完整的实现步骤和代码:
1. 添加依赖
在pubspec.yaml中添加所需的密码学和编码依赖:
dependencies: pointycastle: ^3.7.3 convert: ^3.1.1
2. 实现ES256签名逻辑
下面的代码包含私钥解析、签名生成和Base64 URL编码的完整流程:
import 'dart:convert'; import 'package:pointycastle/pointycastle.dart'; import 'package:pointycastle/ecc/api.dart'; import 'package:pointycastle/signers/ecdsa_signer.dart'; import 'package:pointycastle/digests/sha256.dart'; import 'package:convert/convert.dart'; // 解析PEM格式的P-256私钥(对应Apple开发者后台下载的.p8文件) ECPrivateKey parsePemPrivateKey(String pem) { // 移除PEM格式的头尾标识和换行符 final cleanPem = pem .replaceAll('-----BEGIN PRIVATE KEY-----', '') .replaceAll('-----END PRIVATE KEY-----', '') .replaceAll(RegExp(r'[\n\r]'), ''); // Base64解码得到私钥的DER字节 final privateKeyBytes = base64Decode(cleanPem); // 解析PKCS#8格式的ASN.1结构 final asn1Parser = ASN1Parser(privateKeyBytes); final sequence = asn1Parser.nextObject() as ASN1Sequence; // 提取EC私钥的DER编码部分 final privateKeyOctet = sequence.elements[2] as ASN1OctetString; final ecKeyDer = privateKeyOctet.valueBytes; // 解析EC私钥的具体内容 final ecParser = ASN1Parser(ecKeyDer); final ecSequence = ecParser.nextObject() as ASN1Sequence; final privateKeyInt = ecSequence.elements[1] as ASN1Integer; // 指定P-256曲线参数(prime256v1就是NIST P-256) final domainParams = ECDomainParameters('prime256v1'); return ECPrivateKey(privateKeyInt.value, domainParams); } // 生成符合Apple要求的ES256签名(Base64 URL编码) String generateEs256Signature(String dataToSign, String privateKeyPem) { // 初始化ECDSA签名器,使用SHA-256哈希算法 final signer = ECDSASigner(SHA256Digest(), null); final privateKey = parsePemPrivateKey(privateKeyPem); signer.init(true, PrivateKeyParameter<ECPrivateKey>(privateKey)); // 将待签名字符串转为UTF-8字节 final dataBytes = utf8.encode(dataToSign); // 生成ECDSA签名(返回的是r和s组成的ECSignature对象) final signature = signer.generateSignature(dataBytes) as ECSignature; // 将r和s转为32字节的固定长度数组(P-256曲线的要求),然后拼接 final rBytes = _intToFixedLengthBytes(signature.r, 32); final sBytes = _intToFixedLengthBytes(signature.s, 32); final combinedSignature = Uint8List.fromList([...rBytes, ...sBytes]); // 转为Base64 URL编码,并移除末尾的填充符= return base64UrlEncode(combinedSignature).replaceAll('=', ''); } // 将BigInt转为指定长度的字节数组,不足时前面补0 Uint8List _intToFixedLengthBytes(BigInt value, int length) { final hexStr = value.toRadixString(16).padLeft(length * 2, '0'); return Uint8List.fromList(hex.decode(hexStr)); }
3. 使用示例
- 读取Apple开发者后台下载的
.p8私钥文件内容,作为privateKeyPem参数。 - 构造待签名的字符串:Apple要求签名的是请求URL的路径+查询参数部分(不含协议、域名,比如
/api/v1/snapshot?center=37.7749,-122.4194&size=600x400)。 - 生成签名后,将其追加到URL的查询参数中,比如添加
&signature=生成的签名值。
关键注意事项
- 参数顺序一致:待签名的字符串中的参数顺序必须和最终请求URL的参数顺序完全一致,否则签名验证会失败。
- 私钥安全存储:不要将私钥硬编码在代码中,建议使用
flutter_secure_storage等安全存储库保存。 - 签名格式校验:Apple要求签名是
r+s的原始字节拼接(不是ASN.1编码格式),代码中已经处理了这一点。
内容的提问来源于stack exchange,提问作者Clifton Labrum
相关产品推荐
相关产品推荐

