使用AWS KMS签名以太坊时R/S值异常,无法生成可恢复密钥
解决AWS KMS签名以太坊时
Could not construct a recoverable key异常问题 问题场景
使用AWS KMS服务对以太坊数据签名,执行Sign.createSignatureData(ecdsaSignature,pubKeyToBigInter(publicKey),Hash.sha3(Bytes))时抛出异常:
Could not construct a recoverable key. Are your credentials valid?
初步判断是parseDERSequence方法返回的R、S值不正确导致失败,相关代码见下文。
核心问题分析
- 摘要算法不匹配:Web3j的
Sign.createSignatureData在恢复公钥时,依赖原始数据的Keccak-256哈希(即Hash.sha3(Bytes)),但当前代码传给KMS签名的是原始数据的SHA-256哈希,两者摘要完全不同,导致公钥恢复验证失败。 - S值不符合以太坊规范:AWS KMS生成的ECDSA签名中,S值可能大于secp256k1曲线阶的一半,而以太坊强制要求S为低值(≤曲线阶的一半),否则无法正确恢复公钥。
- 密钥曲线可能不匹配:如果AWS KMS创建的密钥不是secp256k1曲线(以太坊专用曲线),而是默认的secp256r1,也会导致公钥恢复失败。
解决方案
1. 统一摘要算法
将传给KMS的签名消息改为原始数据的Keccak-256哈希,并设置MessageType=RAW,避免KMS再次对消息做SHA-256哈希。
2. 处理S值为低S规范
解析KMS返回的签名后,检查S值是否超过secp256k1曲线阶的一半,若超过则用曲线阶减去S值得到合规的低S值。
3. 确认KMS密钥曲线
确保AWS KMS中创建的密钥是secp256k1曲线类型,而非默认的secp256r1。
修改后的完整代码
import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.regions.Regions; import com.amazonaws.services.kms.AWSKMS; import com.amazonaws.services.kms.AWSKMSClientBuilder; import com.amazonaws.services.kms.model.GetPublicKeyRequest; import com.amazonaws.services.kms.model.SignRequest; import com.amazonaws.util.BinaryUtils; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.encoders.Hex; import org.junit.Test; import org.web3j.crypto.ECDSASignature; import org.web3j.crypto.Hash; import org.web3j.crypto.Keys; import org.web3j.crypto.Sign; import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; @Slf4j public class AwsKmsTest { private static final String keyId = "****"; // secp256k1曲线阶和半阶常量 private static final BigInteger SECP256K1_CURVE_ORDER = new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16); private static final BigInteger SECP256K1_HALF_CURVE_ORDER = new BigInteger("7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0", 16); AWSKMS kmsClient = AWSKMSClientBuilder.standard() .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials("****", "*****"))) .withRegion(Regions.AP_NORTHEAST_1) .build(); static { if (Security.getProvider("BC") == null) { Security.addProvider(new BouncyCastleProvider()); } } @Test public void publicKey() throws Exception { BCECPublicKey publicKey = (BCECPublicKey) getPublicKey(); System.out.println(Keys.getAddress(pubKeyToBigInter(publicKey))); } @Test public void sign() throws Exception { String msg = "hi"; byte[] Bytes = msg.getBytes(StandardCharsets.UTF_8); // 使用Web3j的Keccak-256哈希,和恢复逻辑保持一致 byte[] digestBytes = Hash.sha3(Bytes); SignRequest signRequest = new SignRequest(); signRequest.setKeyId(keyId); signRequest.setMessage(ByteBuffer.wrap(digestBytes)); signRequest.setMessageType("RAW"); // 告知KMS这是已哈希的原始数据 signRequest.setSigningAlgorithm("ECDSA_SHA_256"); ByteBuffer signatureB = kmsClient.sign(signRequest).getSignature(); BCECPublicKey publicKey = (BCECPublicKey) getPublicKey(); // 验证签名(可选,用于确认KMS签名正确性) Signature signature = Signature.getInstance("SHA256withECDSA", "BC"); signature.initVerify(publicKey); signature.update(digestBytes); // 注意这里更新的是Keccak哈希后的数据,因为KMS签名的是这个值 if (signature.verify(signatureB.array())) { ECDSASignature ecdsaSignature = parseDERSequence(signatureB.array()); // 生成以太坊兼容的签名数据 Sign.SignatureData signMessage = Sign.createSignatureData(ecdsaSignature, pubKeyToBigInter(publicKey), Hash.sha3(Bytes)); System.out.println("0x" + Hex.toHexString(signMessage.getR()) + Hex.toHexString(signMessage.getS()) + Hex.toHexString(signMessage.getV())); } } public static BigInteger pubKeyToBigInter(BCECPublicKey publicKey) { byte[] publicKeyBytes = publicKey.getQ().getEncoded(false); return new BigInteger(1, Arrays.copyOfRange(publicKeyBytes, 1, publicKeyBytes.length)); } public PublicKey getPublicKey() throws Exception { GetPublicKeyRequest publicKeyRequest = new GetPublicKeyRequest(); publicKeyRequest.setKeyId(keyId); ByteBuffer publicKeyByteBuffer = kmsClient.getPublicKey(publicKeyRequest).getPublicKey(); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(BinaryUtils.copyBytesFrom(publicKeyByteBuffer)); KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC"); return keyFactory.generatePublic(keySpec); } private ECDSASignature parseDERSequence(byte[] derEncoded) throws IOException { ASN1InputStream asn1InputStream = new ASN1InputStream(derEncoded); ASN1Sequence asn1Sequence = (ASN1Sequence) asn1InputStream.readObject(); ASN1Integer r = (ASN1Integer) asn1Sequence.getObjectAt(0); ASN1Integer s = (ASN1Integer) asn1Sequence.getObjectAt(1); BigInteger rValue = r.getPositiveValue(); BigInteger sValue = s.getPositiveValue(); // 将S值转换为以太坊要求的低S值 if (sValue.compareTo(SECP256K1_HALF_CURVE_ORDER) > 0) { sValue = SECP256K1_CURVE_ORDER.subtract(sValue); } return new ECDSASignature(rValue, sValue); } }
关键修改点说明
- 替换摘要生成逻辑:用
Hash.sha3(Bytes)替代SHA-256哈希,确保和Web3j恢复逻辑一致。 - 修改KMS签名请求:设置
MessageType=RAW,告知KMS直接对传入的Keccak哈希值签名。 - 添加低S值处理:在
parseDERSequence中对S值做合规性转换。 - 调整签名验证逻辑:验证时更新的是Keccak哈希后的数据,匹配KMS的签名对象。
内容的提问来源于stack exchange,提问作者carlos
相关产品推荐
相关产品推荐

