BouncyCastle ECDSA 签名验证使用 prime256v1 和 SHA256withECDSA 算法失败

Posted

技术标签:

【中文标题】BouncyCastle ECDSA 签名验证使用 prime256v1 和 SHA256withECDSA 算法失败【英文标题】:BouncyCastle ECDSA Signature Verification Failed Using prime256v1 and SHA256withECDSA Algorithm 【发布时间】:2016-09-27 02:00:28 【问题描述】:

我需要使用 BouncyCastle 加密提供程序通过 Java 验证 ECDSA 签名。到目前为止,BouncyCastle 未能验证签名。

签名是在Atmel AT88CK590 Crypto Authentication模块中创建的,公钥可以从模块中获取。这是 C/C++ 格式的公钥,长度为 64 个八位字节:

uint8_t pubKey[] = 
    // X coordinate of the elliptic curve.
    0xc1, 0x71, 0xCB, 0xED, 0x65, 0x71, 0x82, 0x2E, 0x8F, 0x8A, 0x43, 0x8D, 0x72, 0x56, 0xD1, 0xC8,
    0x86, 0x3C, 0xD0, 0xBC, 0x7F, 0xCC, 0xE3, 0x6D, 0xE7, 0xB7, 0x17, 0xED, 0x29, 0xC8, 0x38, 0xCB,

    // Y coordinate of the elliptic curve.
    0x80, 0xCD, 0xBE, 0x0F, 0x1D, 0x5C, 0xC5, 0x46, 0x99, 0x24, 0x8F, 0x6E, 0x0A, 0xEA, 0x1F, 0x7A,
    0x43, 0xBA, 0x2B, 0x03, 0x80, 0x90, 0xE9, 0x25, 0xB2, 0xD0, 0xE6, 0x48, 0x93, 0x91, 0x64, 0x83
;

Base64编码的原始消息、签名和公钥:

// Raw message to sign
private static final String tokenStr = "12345678901234567890123456789012";
// Base64 encoded
private static final String pubKeyStr = "wXHL7WVxgi6PikONclbRyIY80Lx/zONt57cX7SnIOMuAzb4PHVzFRpkkj24K6h96
Q7orA4CQ6SWy0OZIk5Fkgw==";
// Base64 encoded
private static final String signatureStr = "XF2WossFTA82ndYFGEH0FPqAldkFQLGd/Bv/Qh8UYip7sXUvCUnFgi1YXjN3WxLn
IwSo3OaHLCOzGAtIis0b3A==";

要转换公钥,我使用以下方法:

private static PublicKey getPublicKeyFromBytes(byte[] pubKey, String ecSpec, String provider)
        throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException 
    ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec(ecSpec);
    KeyFactory kf = KeyFactory.getInstance(ECDSA_CRYPTO, provider);
    ECNamedCurveSpec params = new ECNamedCurveSpec(ecSpec, spec.getCurve(), spec.getG(), spec.getN());
    ECPoint pubPoint =  ECPointUtil.decodePoint(params.getCurve(), pubKey);
    ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(pubPoint, params);
    PublicKey publicKey = kf.generatePublic(pubKeySpec);

    return publicKey;

要将签名转换为 DER 格式,我使用以下方法:

private static byte[] toDERSignature(byte[] tokenSignature) throws IOException 
    byte[] r = Arrays.copyOfRange(tokenSignature, 0, tokenSignature.length / 2);
    byte[] s = Arrays.copyOfRange(tokenSignature, tokenSignature.length / 2, tokenSignature.length);
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    DEROutputStream derOutputStream = new DEROutputStream(byteArrayOutputStream);
    ASN1EncodableVector v = new ASN1EncodableVector();

    v.add(new ASN1Integer(new BigInteger(1, r)));
    v.add(new ASN1Integer(new BigInteger(1, s)));
    derOutputStream.writeObject(new DERSequence(v));

    byte[] derSignature = byteArrayOutputStream.toByteArray();

    return derSignature;

这是验证签名的代码:

Security.addProvider(new BouncyCastleProvider());

byte[] tokenBytes = tokenStr.getBytes("UTF-8");
String urlDecodePubKeyStr = pubKeyStr.replace(newlinehtml, "");
byte[] pubKeyBytes = DatatypeConverter.parseBase64Binary(urlDecodePubKeyStr);
String urlDecodeSignatureStr = signatureStr.replace(newlineHtml, "");
byte[] signBytes = DatatypeConverter.parseBase64Binary(urlDecodeSignatureStr);
byte[] derSignature = toDERSignature(signBytes);
ByteBuffer bb = ByteBuffer.allocate(pubKeyBytes.length + 1);

bb.put((byte)4);
bb.put(pubKeyBytes);

PublicKey ecPublicKey = getPublicKeyFromBytes(bb.array(), "prime256v1", "BC");

System.out.println("\nSignature: " + Hex.toHexString(signBytes).toUpperCase());
System.out.println("DER Signature: " + Hex.toHexString(derSignature).toUpperCase());
System.out.println(ecPublicKey.toString());

Signature signature = Signature.getInstance("SHA256withECDSA", "BC");

signature.initVerify(ecPublicKey);
signature.update(tokenBytes);

boolean result = signature.verify(derSignature);

System.out.println("BC Signature Valid: " + result);

输出:

Signature: 5C5D96A2CB054C0F369DD6051841F414FA8095D90540B19DFC1BFF421F14622A7BB1752F0949C5822D585E33775B12E72304A8DCE6872C23B3180B488ACD1BDC
DER Signature: 304402205C5D96A2CB054C0F369DD6051841F414FA8095D90540B19DFC1BFF421F14622A02207BB1752F0949C5822D585E33775B12E72304A8DCE6872C23B3180B488ACD1BDC
EC Public Key
           X: c171cbed6571822e8f8a438d7256d1c8863cd0bc7fcce36de7b717ed29c838cb
           Y: 80cdbe0f1d5cc54699248f6e0aea1f7a43ba2b038090e925b2d0e64893916483

BC Signature Valid: false

以前有没有人遇到过同样的问题?我在这里错过了什么?

【问题讨论】:

newlineHtml 定义如下: private static final String newlineHtml = " "; 您是否对输入数据(又名消息或明文)进行了二进制比较?你确定公钥和私钥是一对吗?哈希算法呢?曲线呢? 【参考方案1】:

该签名值不像标准和通常那样在散列上计算,而是直接在数据上计算。要么:

Atmel 不散列,因此您必须先散列并提供要签名的散列输出

Atmel 应该散列,但您没有设置一些必要的选项或其他东西

您实际上想要一个不带散列的非传统“原始”签名。请注意,这会将数据限制为 32 或 31 个字节。如果要签名的值可能会受到对手的影响,并且用于签名的随机生成器很弱,​​我认为这会产生一种可以恢复您的私钥的攻击(尽管我还没有通过它)。即使有如果是我也会避免的风险。

无论如何,您所拥有的价值可以使用方案 NoneWithECDSA 进行验证。

此外,在对此进行测试时,我发现您的代码中可能有一些您可能想要也可能不想要的改进。首先,您不需要准编码然后解码该点,因为您有固定格式的 X,Y,您可以使用它们。其次,您确实需要对签名进行 DER 编码,但可以更简单地完成。为了清楚起见,这是我的版本,有两个变化,在一个线性块中:

public static void main (String[] args) throws Exception 
    Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
    // test data; real data would come from outside
    byte[] dataBytes = "12345678901234567890123456789012".getBytes("UTF-8");
    String sigString = "XF2WossFTA82ndYFGEH0FPqAldkFQLGd/Bv/Qh8UYip7sXUvCUnFgi1YXjN3WxLn
IwSo3OaHLCOzGAtIis0b3A==";
    String pubkeyString = "wXHL7WVxgi6PikONclbRyIY80Lx/zONt57cX7SnIOMuAzb4PHVzFRpkkj24K6h96
Q7orA4CQ6SWy0OZIk5Fkgw==";
    String ecSpec="prime256v1"; int size=32; // bytes for x,y in pubkey also r,s in sig

    byte[] pubkeyBytes = DatatypeConverter.parseBase64Binary(pubkeyString.replaceAll("
","") );
    KeyFactory kf = KeyFactory.getInstance ("ECDSA", "BC");
    ECNamedCurveParameterSpec cspec = ECNamedCurveTable.getParameterSpec(ecSpec);
    BigInteger x = new BigInteger(1, Arrays.copyOfRange(pubkeyBytes,0,size));
    BigInteger y = new BigInteger(1, Arrays.copyOfRange(pubkeyBytes,size,size*2));
    ECPublicKeySpec kspec = new ECPublicKeySpec (cspec.getCurve().createPoint(x, y), cspec);
    PublicKey k = kf.generatePublic(kspec);

    byte[] sigBytes = DatatypeConverter.parseBase64Binary(sigString.replaceAll("
","") );
    ASN1EncodableVector v = new ASN1EncodableVector();
    v.add(/*r*/new ASN1Integer(new BigInteger(1, Arrays.copyOfRange(sigBytes,0,size))));
    v.add(/*s*/new ASN1Integer(new BigInteger(1, Arrays.copyOfRange(sigBytes,size,size*2))));
    byte[] sigDer = new DERSequence(v).getEncoded();

    Signature sig = Signature.getInstance("NoneWithECDSA", "BC"); // NOTE None instead of a hash
    sig.initVerify (k); sig.update (dataBytes); 
    System.out.println ("verify="+sig.verify(sigDer));

PS:您将公钥的一半评论为X [Y] coordinate of the elliptic curve。它们是曲线上点的坐标,即公钥;曲线本身没有坐标。

【讨论】:

就是戴夫。我刚刚收到来自 Atmel 的确认,还告诉我芯片没有散列提供的输入。现在在 Java 端使用 NonewithECDSA 验证签名就好了。

以上是关于BouncyCastle ECDSA 签名验证使用 prime256v1 和 SHA256withECDSA 算法失败的主要内容,如果未能解决你的问题,请参考以下文章

验证充气城堡上的 javacard 签名 ALG_ECDSA_SHA

每个椭圆曲线签名生成的不同签名

无法验证签名 (cmssigneddata) bouncycastle

使用 ecdsa 包验证 ECDSA 签名失败

使用带有曲线 secp224k1 的私钥签署 ECDSA

使用 mbedtls 生成的 RSA 签名,无法使用 C# (bouncycastle) 应用程序进行验证