搜索
首页后端开发Golang无法使用 JS 验证 Go 生成的 ECDSA 签名

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

Feb 10, 2024 pm 08:03 PM
go语言区块链移动应用程序

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

php小编小新在使用Go语言生成ECDSA签名时遇到了一个问题,即无法使用JS进行验证。这个问题的解决方法是在Go代码中添加一些附加的字段,以确保签名的正确性。通过对Go代码进行一些修改,我们可以解决这个问题,并使得JS能够正确验证Go生成的ECDSA签名。这篇文章将为您详细介绍具体的解决方法和步骤。

问题内容

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

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

项目概述

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

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

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

数据

  • 签名示例:3045022100f28c29042a6d766810e21f2c0a1839f93140989299cae1d37b49a454373659c802203d0967be0696686414fe2efed3a71bc1639d066 ee127cfb7c0ad369521459d00
  • 公钥:
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEq6iOuQeIhlhywCjo5yoABGODOJRZ
c6/L8XzUYEsocCbc/JHiByGjuB3G9cSU2vUi1HUy5LsCtX2wlHSEObGVBw==
-----END PUBLIC KEY-----
  • 哈希值:bb5dbfcb5206282627254ab23397cda842b082696466f2563503f79a5dccf942

脚本

JS代码
<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>
应用验证码(Flutter Dart)
<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>
密钥生成脚本(Go)
<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>

链接到签名包装器脚本:链接

我的尝试

  • 我已经使用两个新的密钥对(和您的库)进行了测试,以签署一些示例数据,以查看密钥中的内容是否错误,事实并非如此
  • 我已经使用您的库和我的私钥测试了签名数据,并使用我的公钥对其进行了验证,以查看我的私钥是否已损坏,事实并非如此
  • 我已经尝试了网络加密 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 代码中的验证与 Dart 代码不兼容,原因有两个:

  • 首先,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>

以上是无法使用 JS 验证 Go 生成的 ECDSA 签名的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文转载于:stackoverflow。如有侵权,请联系admin@php.cn删除
学习Go Byte Slice操纵:使用'字节”软件包学习Go Byte Slice操纵:使用'字节”软件包May 16, 2025 am 12:14 AM

1)usebybytes.2)

如何使用'编码/二进制”软件包在GO中编码和解码二进制数据(分步)如何使用'编码/二进制”软件包在GO中编码和解码二进制数据(分步)May 16, 2025 am 12:14 AM

Tousethe"encoding/binary"packageinGoforencodinganddecodingbinarydata,followthesesteps:1)Importthepackageandcreateabuffer.2)Usebinary.Writetoencodedataintothebuffer,specifyingtheendianness.3)Usebinary.Readtodecodedatafromthebuffer,againspeci

您如何使用'编码/二进制”软件包在GO中编码和解码二进制数据?您如何使用'编码/二进制”软件包在GO中编码和解码二进制数据?May 16, 2025 am 12:13 AM

encoding/binary包提供了统一的方式来处理二进制数据。1)使用binary.Write和binary.Read函数可以编码和解码整数、浮点数等多种数据类型。2)可以通过实现binary.ByteOrder接口来处理自定义类型。3)需要注意字节序选择、数据对齐和错误处理,以确保数据的正确性和高效性。

Go Strings软件包:每个用例都完成吗?Go Strings软件包:每个用例都完成吗?May 16, 2025 am 12:09 AM

Go的strings包不适用于所有用例。它适用于大多数常见的字符串操作,但对于复杂的NLP任务、正则表达式匹配和特定格式解析,可能需要第三方库。

GO字符串软件包的限制是什么?GO字符串软件包的限制是什么?May 16, 2025 am 12:05 AM

Go语言中的strings包在处理大量字符串操作时存在性能和内存使用上的限制。1)性能问题:如strings.Replace和strings.ReplaceAll在处理大规模字符串替换时效率较低。2)内存使用:由于字符串不可变,每次操作会生成新对象,导致内存消耗增加。3)Unicode处理:在处理复杂Unicode规则时不够灵活,可能需要借助其他包或库。

GO中的字符串操纵:掌握'字符串”软件包GO中的字符串操纵:掌握'字符串”软件包May 14, 2025 am 12:19 AM

掌握Go语言中的strings包可以提高文本处理能力和开发效率。1)使用Contains函数检查子字符串,2)用Index函数查找子字符串位置,3)Join函数高效拼接字符串切片,4)Replace函数替换子字符串。注意避免常见错误,如未检查空字符串和大字符串操作性能问题。

去'字符串”包装提示和技巧去'字符串”包装提示和技巧May 14, 2025 am 12:18 AM

你应该关心Go语言中的strings包,因为它能简化字符串操作,使代码更清晰高效。1)使用strings.Join高效拼接字符串;2)用strings.Fields按空白符分割字符串;3)通过strings.Index和strings.LastIndex查找子串位置;4)用strings.ReplaceAll进行字符串替换;5)利用strings.Builder进行高效字符串拼接;6)始终验证输入以避免意外结果。

GO中的'字符串”软件包:您的首选字符串操作GO中的'字符串”软件包:您的首选字符串操作May 14, 2025 am 12:17 AM

thestringspackageingoisesential forefficientstringManipulation.1)itoffersSimpleyetpoperfulfunctionsFortaskSlikeCheckingSslingSubstringsStringStringsStringsandStringsN.2)ithandhishiCodeDewell,withFunctionsLikestrings.fieldsfieldsfieldsfordsforeflikester.fieldsfordsforwhitespace-fieldsforwhitespace-separatedvalues.3)3)

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

MinGW - 适用于 Windows 的极简 GNU

MinGW - 适用于 Windows 的极简 GNU

这个项目正在迁移到osdn.net/projects/mingw的过程中,你可以继续在那里关注我们。MinGW:GNU编译器集合(GCC)的本地Windows移植版本,可自由分发的导入库和用于构建本地Windows应用程序的头文件;包括对MSVC运行时的扩展,以支持C99功能。MinGW的所有软件都可以在64位Windows平台上运行。

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

EditPlus 中文破解版

EditPlus 中文破解版

体积小,语法高亮,不支持代码提示功能

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一个PHP/MySQL的Web应用程序,非常容易受到攻击。它的主要目标是成为安全专业人员在合法环境中测试自己的技能和工具的辅助工具,帮助Web开发人员更好地理解保护Web应用程序的过程,并帮助教师/学生在课堂环境中教授/学习Web应用程序安全。DVWA的目标是通过简单直接的界面练习一些最常见的Web漏洞,难度各不相同。请注意,该软件中