kjur / jsrsasign

The 'jsrsasign' (RSA-Sign JavaScript Library) is an opensource free cryptography library supporting RSA/RSAPSS/ECDSA/DSA signing/validation, ASN.1, PKCS#1/5/8 private/public key, X.509 certificate, CRL, OCSP, CMS SignedData, TimeStamp, CAdES and JSON Web Signature/Token in pure JavaScript.
https://kjur.github.io/jsrsasign
Other
3.24k stars 646 forks source link

Can't verify valid ECDSA singature #602

Closed Flajt closed 9 months ago

Flajt commented 9 months ago

Hello there,

I've got a small issue, (My assumption is that there is one small thing holding me back but I have no clue what), as mentioned in the title.

I will start by giving an overview about what I'm doing and will provide everything I have afterwards.

Project overview

I'm hashing files with SHA-256 in a mobile app and sign the hash in the backend with an ECDSA P-256 key. This is then persisted. If the user needs to he can verify the integretify of the file, by basically hashing the file again and looking up the hash and getting the hash, some metadata and the signature back.

To validate the data has been submitted my app and not a third party (the hashs are persisted in a blockchain but that doesn't really matter for this issue), the app will attempt to verify the signature with the public key. This works fine.

Now I would like to add this option to my website as well, however here is the issue. My signatures are not valid if I use the jsrsasign or thewebcrypto api.

Data

Scripts

JS Code ```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); } ```
App Verification Code (Flutter Dart) ```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(loadAndPrepPubKey())); Uint8List messageAsBytes = Uint8List.fromList(utf8.encode(hash)); return signer.verifySignature(messageAsBytes, convertedSig); } } ```
Key generation script (Go) ```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 to signature wrapper script: Link

My Attemps

My last bet was the 4th attempt, since, from my understanding at least, if you are using the regular way (which I'm doing in the script above) your data get's hashed, which, in my case, is counter productive as I've already gotten a hash so if it's hashed twice it ,of course, won't match. However for reasons I don't get I still get false as return value.

If you have any idea I would appreciate it.

kjur commented 9 months ago

When hashData is a hexadecimal string, sig.updateHex(hashData) instead of sig.updateString(hashData).

Flajt commented 9 months ago

As in the provided JS sample, I've done that but with no success (I guess I should rename hashData to hash so it's clearer what it is).

kjur commented 9 months ago

This code shall work. However your signature or hash seems wrong.

var rs = require("jsrsasign");

var hSig = "3045022100f28c29042a6d766810e21f2c0a1839f93140989299cae1d37b49a454373659c802203d0967be0696686414fe2efed3a71bc1639d066ee127cfb7c0ad369521459d00";
var pubpem = `
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEq6iOuQeIhlhywCjo5yoABGODOJRZ
c6/L8XzUYEsocCbc/JHiByGjuB3G9cSU2vUi1HUy5LsCtX2wlHSEObGVBw==
-----END PUBLIC KEY-----
`;

var hash = "bb5dbfcb5206282627254ab23397cda842b082696466f2563503f79a5dccf942";

var pub = rs.KEYUTIL.getKey(pubpem);
console.log(pub.verifyWithMessageHash(hash, hSig));
Flajt commented 9 months ago

This is what I thought as well, however I can validate everything with the app code... this is what irritates me, how can it work there but not here. I would have assumed that it doesn't work in both cases, however it does in one...

Flajt commented 9 months ago

To add additional info: I've traced the whole thing through my app both hash and signature are valid. So I really have no clue.

Flajt commented 9 months ago

I've created a small go script which should validate the signature if I've done everything correctly and here it works as well given the key, hash and signature.

Click to view script (Go) ```go package main import ( "bufio" "crypto/ecdsa" "crypto/x509" "encoding/hex" "encoding/pem" "flag" "fmt" "os" ) func main() { var signature string var hash string var pubKeyFilePath string flag.StringVar(&signature, "signature", "", "Your signature") flag.StringVar(&hash, "hash", "", "Your hash") flag.StringVar(&pubKeyFilePath, "pbKey", "./pub.pem", "Public Key file path (.pem)") flag.Parse() err := verifySignature(signature, hash, pubKeyFilePath) if err != nil { fmt.Printf("Something went wrong %d", err) return } } func verifySignature(signature string, hash string, pubKeyFilePath string) error { file, err := os.Open(pubKeyFilePath) if err != nil { return err } defer file.Close() pemFileInfo, err := file.Stat() if err != nil { return err } var size int64 = pemFileInfo.Size() pemBytes := make([]byte, size) buffer := bufio.NewReader(file) _, err = buffer.Read(pemBytes) if err != nil { return err } //parse pem file for block, rest := pem.Decode(pemBytes); block != nil; block, rest = pem.Decode(rest) { if block.Type == "EC PUBLIC KEY" || block.Type == "PUBLIC KEY" || block.Type == "EC Public KEY" { pubKey, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return err } ecdsaPubKey, ok := pubKey.(*ecdsa.PublicKey) if !ok { return fmt.Errorf("failed to parse ECDSA public key") } decodeSig, err := hex.DecodeString(signature) if err != nil { return err } isValid := ecdsa.VerifyASN1(ecdsaPubKey, []byte(hash), decodeSig) fmt.Printf("\nIs signature valid: %v \n", isValid) fmt.Println() } } return nil } ```

If I run it with go run validate_signature.go -hash bb5dbfcb5206282627254ab23397cda842b082696466f2563503f79a5dccf942 -signature 3045022100f28c29042a6d766810e21f2c0a1839f93140989299cae1d37b49a454373659c802203d0967be0696686414fe2efed3a71bc1639d066ee127cfb7c0ad369521459d00 -pbKey ./my_key.pem

It returns valid, maybe it's a JS related issue? Since I didn't mange to get it to work in web crypto as well. Or I've screwed up somewhere else.

Flajt commented 8 months ago

I think I got it now. The issue is that I've singned the whole thing as utf-8 decoded bytes, so hex-> utf-8 -> bytes -> signature and not hex -> bytes -> signature. This explains why it didn't work with sig.updateStringData and verifyWithMessageHash the first one would hash my string and not decode it, the second one would just decode the hex to bytes and not to utf-8 bytes.

Thanks regardless