Unable to verify Go generated ECDSA signature using JS
php editor Xiaoxin encountered a problem when using Go language to generate ECDSA signatures, that is, JS could not be used for verification. The solution to this problem is to add some additional fields to the Go code to ensure the correctness of the signature. By making some modifications to the Go code, we can solve this problem and enable JS to correctly verify the ECDSA signature generated by Go. This article will introduce you to the specific solutions and steps in detail.
Question content
I ran into a small problem, (my assumption is that there is a small thing holding me back, but I don't know what), as stated in the title.
I'll start by outlining what I'm doing and then provide everything I have.
Project Overview
I'm hashing the file using SHA-256
in the mobile app and signing the hash on the backend using the ECDSA P-256
key. And then this goes on and on. If the user wants, he can verify the integrity of the file by hashing the file again and looking up the hash and getting the hash, some metadata, and the signature.
In order to verify that the data was submitted to my application and not a third party (the hashes remain in the blockchain, but that is not important for this problem), the application will try to verify the signature using the public key. This work is good.
Now I want to add this option to my website as well, but the problem is. If I use the jsrsasign
or webcrypto
api, my signature is invalid.
data
- Signature example:
3045022100f28c29042a6d766810e21f2c0a1839f93140989299cae1d37b49a454373659c802203d0967be0696686414fe2efed3a71bc1639d066 ee1 27cfb7c0ad369521459d00
- Public key:
-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEq6iOuQeIhlhywCjo5yoABGODOJRZ c6/L8XzUYEsocCbc/JHiByGjuB3G9cSU2vUi1HUy5LsCtX2wlHSEObGVBw== -----END PUBLIC KEY-----
- Hash value:
bb5dbfcb5206282627254ab23397cda842b082696466f2563503f79a5dccf942
script
JS code<code>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); } </code>
<code>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); } } </code>
<code>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 } </code>
Link to signature wrapper script: link
My attempt
- I've tested using two new key pairs (and your library) to sign some sample data to see if what's in the key is wrong, and it's not the case
- I have tested the signed data using your library and my private key and verified it with my public key to see if my private key is corrupt and it is not the case
- I have tried all operations of the Network Encryption API without success
- I tried loading the
ECDSA
public key and usingnew KJUR.crypto.ECDSA({"curve":"secp256r1"}).verifyHex(hash,signature,pubKeyHex)
with With the above data, it doesn't work (only tested in browser console) - I used Firefox and Safari to see if there were any differences, but nothing changed
- I tried passing the hash value as a string via
sig.updateString(hashData)
without success - There are some other smaller changes
- Comparing the hashes, r&s signatures on the website and the app website, everything is as expected.
- I have tracked the entire process from front-end to back-end and no data has changed
My last attempt was the fourth because at least from my understanding if you use the regular way (which I did in the script above) your data will be hashed, as far as I'm concerned In terms of this, it's the opposite of productive, because I've already got the hash value, so if it's hashed twice, of course, it won't match. But for reasons I don't understand, I still get false as the return value.
One last thought, if using P-256
signature, could the problem be that the go ecdsa library truncates the message to 32 bytes? Maybe not so in JS?
Workaround
Validation in JavaScript code is incompatible with Dart code for two reasons:
- 首先,JavaScript代码使用
KJUR.crypto.Signature ()
,它隐式对数据进行哈希处理。由于数据已经被散列,这会导致双重散列。在 Dart 方面,不会发生隐式哈希(因为ECDSASigner()
)。
为了避免 JavaScript 端的隐式哈希并与 Dart 代码兼容,KJUR.crypto.ECDSA()
可以用来代替KJUR.crypto.Signature()
。 - 其次,JavaScript 代码中的
updateHex()
对十六进制编码的哈希值执行十六进制解码,而在 Dart 代码中,十六进制编码的哈希值是 UTF-8 编码的。
为了与 Dart 代码兼容,十六进制编码的哈希值在 JavaScript 代码中也必须采用 UTF-8 编码。
以下 JavaScript 代码解决了这两个问题:
(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>
The above is the detailed content of Unable to verify Go generated ECDSA signature using JS. For more information, please follow other related articles on the PHP Chinese website!

Hot AI Tools

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Undress AI Tool
Undress images for free

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

Hot Topics











Applying for the Huobi API interface requires six steps: 1. Register a Huobi account and select "Global Station"; 2. Complete real-name authentication (L1/L2/L3); 3. Log in and enter the API management page; 4. Create an API key, fill in basic information and select permissions; 5. Generate and save Access Key and Secret Key; 6. Integrate API to the application, pay attention to security matters and change the key regularly.

From the early stages to the present, BNB has shown significant growth. Although its value continues to rise, people have asked what heights BNB can reach. This guide will explore factors that affect the price of Binance Coin. We will discuss its recent performance and make predictions for the next few years. Binance Coin Trend Prediction: How high will BNB rise? What is BNB? BNB is a cryptocurrency of Binance Exchange (official registration) (official download), and Binance is the world's leading cryptocurrency trading platform. It debuted on Ethereum as an ERC-20 token in 2017 and moved to Binance Chain in 2019. Initially, BNB was designed for Binance

The VBILL Token Fund was launched on May 13, providing institutional investors with the opportunity to tokenize U.S. fiscal bills. Vaneck's new tagged fund will enable institutional investors to invest in U.S. Treasury bills. The VBILL Token Fund was launched on May 13 as the result of a cooperation with Securities, a tokenized platform that supports the entire operation of the fund. The fund will be subscribed on four major blockchains – Ethereum, Sorana, Avalanche and BNB Chain. On three of these blockchains (Avalanche, Solana and BNB chains), the minimum subscription is $100,000. On Ethereum, the minimum investment is US$1 million. The fund is Vaneck's efforts to expand its real-life

As the world's leading cryptocurrency trading platform, Binance's mobile app provides users with a convenient trading experience. Whether you are a novice or a veteran trader, Binance’s mobile app is ready to meet your needs. This tutorial will introduce in detail how to download and install the latest Binance v2.99.5 mobile application and complete the registration process. Please note that the download links provided in this article are all official links, and you can download them safely using the download links of this article, without worrying about security issues.

Each investment team has shown great interest and enthusiasm for the project’s highly anticipated NFT Gaming multiverse, and they are still eager to support PolkaWar’s future in the cryptocurrency space. PolkaWar expects institutions and organized funds to have greater enthusiasm for early stage investment, and the company thanks everyone for their sponsorship and support to date. Funds raised will provide the ability to quickly deliver the roadmap and deploy projects in a shorter time. PolkaWar has unique advantages that can benefit from the upcoming surge and excitement. Investors want to know what the PWAR currency contract address is? Let me introduce the PWAR coins to you

In the rapidly developing world of decentralized finance (DeFi), innovation never stops. The latest wave is not just about speed or safety – it is about wisdom. This is exactly when XploraDEX comes into the stage. XploraDEX, built on the XRP record, is setting new benchmarks for decentralized exchanges by introducing artificial intelligence (AI) to every level of trading. But what exactly is XploraDEX, how is it different from other platforms, and what role the $XPL token plays in its ecosystem? Let's parse step by step in an easy-to-understand way.

Ultra-liquidity (HYPE) price update shows that the Bulls are targeting $25 after public interest soared to a record $697 million. Super Liquidity (HYPE) Bulls targeted $25 as token trading is higher than the main support at $19.24. Open interest soared to a record $697 million, indicating strong momentum. Technical signals from RSI (14) and MACD (12,26) show that there may be more room for upwards. By 2030, the BitGet Token (BGB) price forecast targets $25 as exchanges expand and burn tokens worldwide. Web3AI's pre-sales are starting to attract attention, starting at just $0.03 and offering 1,333 times

Uzbekistan is experimenting with a new digital asset, Humo tokens secured by government bonds. The token is pegged to the national currency, and 1 Humo equals 1000 sum. The project is under implementation under Uzbekistan’s legal framework in the field of crypto assets. Several strategic partners have participated in its development, including the Humo payment system that serves 35 million cardholders in Uzbekistan. Thanks to Humo's extensive cooperation with commercial banks, markets and retail structures, the conditions have been created for the widespread use of tokens in daily transactions. The technical basis of the project is provided by Asterium and Broxus. The project adopts the Tycho blockchain protocol developed by Broxus. Its characteristics are high transaction speed and low transaction
