php小編小新在使用Go語言生成ECDSA簽名時遇到了一個問題,即無法使用JS進行驗證。這個問題的解決方法是在Go代碼中添加一些附加的字段,以確保簽名的正確性。通過對Go代碼進行一些修改,我們可以解決這個問題,并使得JS能夠正確驗證Go生成的ECDSA簽名。這篇文章將為您詳細介紹具體的解決方法和步驟。
問題內容
我遇到了一個小問題,(我的假設是有一件小事情阻礙了我,但我不知道是什么),如標題中所述。
我將首先概述我正在做的事情,然后提供我所擁有的一切。
項目概述
我在移動應用程序中使用 SHA-256
對文件進行哈希處理,并使用 ECDSA P-256
密鑰在后端對哈希進行簽名。然后這種情況就一直持續下去。如果用戶需要,他可以通過再次散列文件并查找散列并獲取散列、一些元數據和簽名來驗證文件的完整性。
為了驗證數據已提交給我的應用程序而不是第三方(哈希值保留在區塊鏈中,但這對于此問題并不重要),應用程序將嘗試使用公鑰驗證簽名。這工作很好。
現在我也想將此選項添加到我的網站,但問題是。如果我使用 jsrsasign
或 webcrypto
api,我的簽名無效。
數據
簽名示例:3045022100f28c29042a6d766810e21f2c0a1839f93140989299cae1d37b49a454373659c802203d0967be0696686414fe2efed3a71bc1639d066 ee127cfb7c0ad369521459d00
公鑰:
-----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(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
}
登錄后復制
鏈接到簽名包裝器腳本:鏈接
我的嘗試
我已經使用兩個新的密鑰對(和您的庫)進行了測試,以簽署一些示例數據,以查看密鑰中的內容是否錯誤,事實并非如此
我已經使用您的庫和我的私鑰測試了簽名數據,并使用我的公鑰對其進行了驗證,以查看我的私鑰是否已損壞,事實并非如此
我已經嘗試了網絡加密 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) })();
登錄后復制
登錄后復制