无法用JS验证Go生成的ECDSA签名

问题描述 投票:0回答:1

我有一个小问题,(我的假设是有一件小事情阻碍了我,但我不知道是什么),如标题中所述。

我将首先概述我正在做的事情,然后提供我所拥有的一切。

项目概况

我在移动应用程序中使用

SHA-256
对文件进行哈希处理,并使用
ECDSA P-256
键在后端对哈希进行签名。然后这种情况就一直持续下去。如果用户需要,他可以通过再次散列文件并查找散列并获取散列、一些元数据和签名来验证文件的完整性。

为了验证数据已提交给我的应用程序而不是第三方(哈希值保留在区块链中,但这对于此问题并不重要),应用程序将尝试使用公钥验证签名。这工作很好。

现在我也想将此选项添加到我的网站,但问题是。如果我使用

jsrsasign
webcrypto
api,我的签名无效。

数据

  • 签名样本:
    3045022100f28c29042a6d766810e21f2c0a1839f93140989299cae1d37b49a454373659c802203d0967be0696686414fe2efed3a71bc1639d066ee127cfb7c0ad369521459d00
  • 公钥:
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEq6iOuQeIhlhywCjo5yoABGODOJRZ
c6/L8XzUYEsocCbc/JHiByGjuB3G9cSU2vUi1HUy5LsCtX2wlHSEObGVBw==
-----END PUBLIC KEY-----
  • 哈希:
    bb5dbfcb5206282627254ab23397cda842b082696466f2563503f79a5dccf942

脚本

JS代码
const validHash = document.getElementById("valid-hash");
const locationEmbedded = document.getElementById("location-embedded")
const signatureValid = document.getElementById("valid-sig")
const fileSelector = document.getElementById('file-upload');
const mcaptchaToken = document.getElementById("mcaptcha__token")
const submission = document.getElementById("submission")
let publicKey;

fileSelector.addEventListener("change", (event) => {
    document.getElementsByClassName("file-upload-label")[0].innerHTML = event.target.files[0].name
})
submission.addEventListener('click', async (event) => {
    let token = mcaptchaToken.value
    if (token == null || token == "") {
        alert("Please activate the Captcha!")
        return
    }
    const fileList = fileSelector.files;
    if (fileList[0]) {
        const file = fileList[0]
        const fileSize = file.size;
        let fileData = await readBinaryFile(file)
        let byteArray = new Uint8Array(fileData);
        const bytes = await hashFile(byteArray)
        try {
            let resp = await callApi(toHex(bytes), token)
            validHash.innerHTML = "\u2713"

            const mediainfo = await MediaInfo({ format: 'object' }, async (mediaInfo) => { // Taken from docs
                mediaInfo.analyzeData(() => file.size, (chunkSize, offset) => {
                    return new Promise((resolve, reject) => {
                        const reader = new FileReader()
                        reader.onload = (event) => {
                            if (event.target.error) {
                                reject(event.target.error)
                            }
                            resolve(new Uint8Array(event.target.result))
                        }
                        reader.readAsArrayBuffer(file.slice(offset, offset + chunkSize))
                    })
                })
                try {
                    let tags = mediaInfo.media.track[0].extra
                    latitude = tags.LATITUDE
                    longitude = tags.LONGITUDE
                    if (latitude && longitude) {
                        locationEmbedded.innerHTML = "\u2713"
                    } else {
                        locationEmbedded.innerHTML = "\u2717"
                    }
                } catch (e) {
                    locationEmbedded.innerHTML = "\u2717"
                }
               
            })
            if (publicKey == undefined) {
                let req = await fetch("/publickey")
                if (req.ok) {
                    publicKey = await req.text()
                } else {
                    throw "Could not get public key"
                }
            }

            let signature = resp.data.comment
            if (signature == null || signature == "") {
                throw "No signature found"
            }
            //const timeStamps = resp.data.timestamps
            const hashString = resp.data.hash_string
            console.log(hashString)
            if (hashString !== toHex(bytes)) {
                validHash.innerHTML = "\u2717"
            } else {
                validHash.innerHTML = "\u2713"
            }
            const result = await validateSignature(publicKey, signature, hashString)
            console.log("Valid signature: " + result)
            if (result) {
                signatureValid.innerHTML = "\u2713"
            } else {
                signatureValid.innerHTML = "\u2717"
            }
            mcaptchaToken.value = ""
        } catch (e) {
            alert("Error: " + e)
            window.location.reload()
        }

    } else {
        alert("No file selected");
    }
});


function toHex(buffer) {
    return Array.prototype.map.call(buffer, x => ('00' + x.toString(16)).slice(-2)).join('');
}

async function callApi(hash, token) {
    const url = "/verify";
    let resp = await fetch(url, {
        headers: { "X-MCAPTCHA-TOKEN": token },
        method: "POST",
        body: JSON.stringify({ hash: hash })
    })
    if (resp.ok) {
        return await resp.json();
    } else {
        if (resp.status == 401) {
            throw resp.status
        } else {
            console.log(resp)
            throw "Your hash is either invalid or has not been submitted via the Decentproof App!"
        }
    }
}

async function hashFile(byteArray) {
    let hashBytes = await window.crypto.subtle.digest('SHA-256', byteArray);
    return new Uint8Array(hashBytes)
}

async function validateSignature(key, signature,hashData) {
    const importedKey = importPublicKey(key)
    const sig = new KJUR.crypto.Signature({"alg": "SHA256withECDSA"});
    sig.init(importedKey)
    sig.updateHex(hashData);
    return sig.verify(signature)
}

function readBinaryFile(file) {
    return new Promise((resolve, reject) => {
        var fr = new FileReader();
        fr.onload = () => {
            resolve(fr.result)
        };
        fr.readAsArrayBuffer(file);
    });
}

function importPublicKey(pem) {
    console.log(pem)
    return KEYUTIL.getKey(pem); 
  }

  function hexToBytes(hex) {
    for (var bytes = [], c = 0; c < hex.length; c += 2)
    bytes.push(parseInt(hex.substr(c, 2), 16));
    return new Uint8Array(bytes);
}
应用验证码(Flutter Dart)
import 'dart:convert';
import 'package:convert/convert.dart';
import 'dart:typed_data';
import 'package:basic_utils/basic_utils.dart';
import 'package:decentproof/features/verification/interfaces/ISignatureVerifcationService.dart';
import 'package:pointycastle/asn1/asn1_parser.dart';
import 'package:pointycastle/asn1/primitives/asn1_integer.dart';
import 'package:pointycastle/signers/ecdsa_signer.dart';

class SignatureVerificationService implements ISignatureVerificationService {
  late final ECPublicKey pubKey;
  SignatureVerificationService() {
    pubKey = loadAndPrepPubKey();
  }

  final String pemPubKey = """
-----BEGIN EC PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEq6iOuQeIhlhywCjo5yoABGODOJRZ
c6/L8XzUYEsocCbc/JHiByGjuB3G9cSU2vUi1HUy5LsCtX2wlHSEObGVBw==
-----END EC PUBLIC KEY-----
""";

  ECSignature loadAndConvertSignature(String sig) {
    //Based on: https://github.com/bcgit/pc-dart/issues/159#issuecomment-1105689978
    Uint8List bytes = Uint8List.fromList(hex.decode(sig));
    ASN1Parser p = ASN1Parser(bytes);
    //Needs to be dynamic or otherwise throws odd errors
    final seq = p.nextObject() as dynamic;
    ASN1Integer ar = seq.elements?[0] as ASN1Integer;
    ASN1Integer as = seq.elements?[1] as ASN1Integer;
    BigInt r = ar.integer!;
    BigInt s = as.integer!;
    return ECSignature(r, s);
  }

  ECPublicKey loadAndPrepPubKey() {
    return CryptoUtils.ecPublicKeyFromPem(pemPubKey);
  }

  @override
  bool verify(String hash, String sig) {
    ECSignature convertedSig = loadAndConvertSignature(sig);
    final ECDSASigner signer = ECDSASigner();
    signer.init(false, PublicKeyParameter<ECPublicKey>(loadAndPrepPubKey()));
    Uint8List messageAsBytes = Uint8List.fromList(utf8.encode(hash));
    return signer.verifySignature(messageAsBytes, convertedSig);
  }
}
密钥生成脚本(Go)
package main

import (
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/rand"
    "crypto/x509"
    "encoding/pem"
    "flag"
    "fmt"
    "os"
)

func main() {
    var outPutDir string
    var outPutFileName string
    flag.StringVar(&outPutDir, "out", "./", "Output directory")
    flag.StringVar(&outPutFileName, "name", "key", "Output file name e.g key, my_project_key etc. Adding .pem is not needed")
    flag.Parse()
    key, err := generateKeys()
    if err != nil {
        fmt.Printf("Something went wrong %d", err)
        return
    }
    err = saveKeys(key, outPutDir, outPutFileName)
    if err != nil {
        fmt.Printf("Something went wrong %d", err)
        return
    }
    fmt.Printf("Keys generated and saved to %s%s.pem and %spub_%s.pem", outPutDir, outPutFileName, outPutDir, outPutFileName)

}

func generateKeys() (*ecdsa.PrivateKey, error) {
    return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}
func saveKeys(key *ecdsa.PrivateKey, outPutDir string, outPutFileName string) error {
    bytes, err := x509.MarshalECPrivateKey(key)
    if err != nil {
        return err
    }
    privBloc := pem.Block{Type: "EC PRIVATE KEY", Bytes: bytes}
    privKeyFile, err := os.Create(outPutDir + outPutFileName + ".pem")
    if err != nil {
        return err
    }
    defer privKeyFile.Close()
    err = pem.Encode(privKeyFile, &privBloc)
    if err != nil {
        return err
    }

    bytes, err = x509.MarshalPKIXPublicKey(&key.PublicKey)
    pubBloc := pem.Block{Type: "EC Public KEY", Bytes: bytes}
    pubKeyFile, err := os.Create(outPutDir + "pub_" + outPutFileName + ".pem")
    if err != nil {
        return err
    }
    defer pubKeyFile.Close()
    err = pem.Encode(pubKeyFile, &pubBloc)
    if err != nil {
        return err
    }

    return nil
}

签名包装脚本的链接:Link

我的尝试

  • 我已经使用两个新的密钥对(和你的库)进行了测试,以签署一些示例数据,以查看密钥的某些内容是否错误,事实并非如此
  • 我已经使用您的库和我的私钥测试了签名数据,并使用我的公钥对其进行了验证,以查看我的私钥是否已损坏,事实并非如此
  • 我已经尝试了网络加密 API 的全部操作,但没有成功
  • 我尝试加载
    ECDSA
    公钥并将
    new KJUR.crypto.ECDSA({"curve":"secp256r1"}).verifyHex(hash, signature,pubKeyHex)
    与上述数据一起使用,但它不起作用(仅在浏览器控制台中测试)
  • 我用过 Firefox 和 Safari 看看有什么不同,没有改变任何东西
  • 我尝试通过
    sig.updateString(hashData)
    将哈希值作为字符串传递,但没有成功
  • 还有其他一些较小的变化
  • 比较网站和应用程序网站上的哈希、r & s + 签名,一切都匹配。
  • 我从前端到后端追踪了整个过程,没有数据变化

我的最后一次尝试是第四次尝试,因为至少从我的理解来看,如果您使用常规方式(我在上面的脚本中所做的),您的数据会被散列,就我而言,这会适得其反,因为我已经得到了一个哈希值,所以如果它被哈希两次,当然,它不会匹配。但是由于我不明白的原因,我仍然得到 false 作为返回值。

最后一个想法,如果使用

P-256
签名,问题是否可能是 go ecdsa 库将消息截断为 32 字节?也许在 JS 中则不然?

javascript flutter go cryptography ecdsa
1个回答
0
投票

ECDSASigner()
中,没有指定摘要,这就是为什么数据没有隐式散列(这是正确的,因为数据已经散列)。相反,
KJUR.crypto.Signature()
散列隐式,所以这种实现是不合适的。
正确的实现是
KJUR.crypto.ECDSA()
,它允许低级别签名而无需隐式哈希。

以下示例使用

KJUR.crypto.ECDSA()
实现来验证发布的数据:

(async () => {

var spki = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEq6iOuQeIhlhywCjo5yoABGODOJRZ
c6/L8XzUYEsocCbc/JHiByGjuB3G9cSU2vUi1HUy5LsCtX2wlHSEObGVBw==
-----END PUBLIC KEY-----`; 
var pubkey = KEYUTIL.getKey(spki).getPublicKeyXYHex()
var pubkeyHex = '04' + pubkey.x + pubkey.y

var msgHashHex = ArrayBuffertohex(new TextEncoder().encode("bb5dbfcb5206282627254ab23397cda842b082696466f2563503f79a5dccf942").buffer)
// var msgHashHex = ArrayBuffertohex(new TextEncoder().encode("bb5dbfcb5206282627254ab23397cda8").buffer); // works also since only the first 32 bytes are considered for P-256

var sigHex = "3045022100f28c29042a6d766810e21f2c0a1839f93140989299cae1d37b49a454373659c802203d0967be0696686414fe2efed3a71bc1639d066ee127cfb7c0ad369521459d00"

var ec = new KJUR.crypto.ECDSA({'curve': 'secp256r1'})
var verified = ec.verifyHex(msgHashHex, sigHex, pubkeyHex)
console.log("Verification:", verified)
 
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsrsasign/10.4.0/jsrsasign-all-min.js"></script>

现在验证成功了。


请注意,Dart 代码中的哈希值是 UTF-8 编码的。 Utf8 编码提供 64 字节的数据,这就是为什么 P-256 只考虑前 32 字节,即忽略最后 32 字节。
这可能是 Dart 代码中的错误或错误的设计。正确的是十六进制解码,它将提供 32 个字节,以便考虑整个数据。

© www.soinside.com 2019 - 2024. All rights reserved.