일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- git rebase
- 깃허브명령어
- SBT표준
- 체인의정석
- 러스트 기초 학습
- 스마트컨트렉트 예약어 함수이름 중복
- 스마트 컨트렉트 함수이름 중복
- 컨트렉트 배포 자동화
- multicall
- ethers websocket
- 스마트컨트렉트테스트
- rust 기초
- ethers type
- nest.js설명
- 프록시배포구조
- ambiguous function description
- ethers
- Vue
- nestjs 튜토리얼
- chainlink 설명
- 스마트컨트렉트프록시
- 스마트컨트렉트 함수이름 중복 호출
- 컨트렉트 동일한 함수이름 호출
- ethers typescript
- 러스트기초
- 머신러닝기초
- ethers v6
- 러스트 기초
- Vue.js
- vue기초
- Today
- Total
체인의정석
Solidity 컨트렉트에서 서명 검증하기 본문
이번에 서명 검증 로직을 구현하면서 오랜만에 활용 및 정리를 해봤다.
기본 예제 학습
일단 아래 예제가 검증 로직을 이해하기에 정말 편리하고 코드도 직관적이다.
https://solidity-by-example.org/signature/
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/* Signature Verification
How to Sign and Verify
# Signing
1. Create message to sign
2. Hash the message
3. Sign the hash (off chain, keep your private key secret)
# Verify
1. Recreate hash from the original message
2. Recover signer from signature and hash
3. Compare recovered signer to claimed signer
*/
contract VerifySignature {
/* 1. Unlock MetaMask account
ethereum.enable()
*/
/* 2. Get message hash to sign
getMessageHash(
0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C,
123,
"coffee and donuts",
1
)
hash = "0xcf36ac4f97dc10d91fc2cbb20d718e94a8cbfe0f82eaedc6a4aa38946fb797cd"
*/
function getMessageHash(
address _to,
uint256 _amount,
string memory _message,
uint256 _nonce
) public pure returns (bytes32) {
return keccak256(abi.encodePacked(_to, _amount, _message, _nonce));
}
/* 3. Sign message hash
# using browser
account = "copy paste account of signer here"
ethereum.request({ method: "personal_sign", params: [account, hash]}).then(console.log)
# using web3
web3.personal.sign(hash, web3.eth.defaultAccount, console.log)
Signature will be different for different accounts
0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
*/
function getEthSignedMessageHash(bytes32 _messageHash)
public
pure
returns (bytes32)
{
/*
Signature is produced by signing a keccak256 hash with the following format:
"\x19Ethereum Signed Message\n" + len(msg) + msg
*/
return keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash)
);
}
/* 4. Verify signature
signer = 0xB273216C05A8c0D4F0a4Dd0d7Bae1D2EfFE636dd
to = 0x14723A09ACff6D2A60DcdF7aA4AFf308FDDC160C
amount = 123
message = "coffee and donuts"
nonce = 1
signature =
0x993dab3dd91f5c6dc28e17439be475478f5635c92a56e17e82349d3fb2f166196f466c0b4e0c146f285204f0dcb13e5ae67bc33f4b888ec32dfe0a063e8f3f781b
*/
function verify(
address _signer,
address _to,
uint256 _amount,
string memory _message,
uint256 _nonce,
bytes memory signature
) public pure returns (bool) {
bytes32 messageHash = getMessageHash(_to, _amount, _message, _nonce);
bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);
return recoverSigner(ethSignedMessageHash, signature) == _signer;
}
function recoverSigner(
bytes32 _ethSignedMessageHash,
bytes memory _signature
) public pure returns (address) {
(bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature);
return ecrecover(_ethSignedMessageHash, v, r, s);
}
function splitSignature(bytes memory sig)
public
pure
returns (bytes32 r, bytes32 s, uint8 v)
{
require(sig.length == 65, "invalid signature length");
assembly {
/*
First 32 bytes stores the length of the signature
add(sig, 32) = pointer of sig + 32
effectively, skips first 32 bytes of signature
mload(p) loads next 32 bytes starting at the memory address p into memory
*/
// first 32 bytes, after the length prefix
r := mload(add(sig, 32))
// second 32 bytes
s := mload(add(sig, 64))
// final byte (first byte of the next 32 bytes)
v := byte(0, mload(add(sig, 96)))
}
// implicitly return (r, s, v)
}
}
example
https://github.com/t4sk/hello-erc20-permit/blob/main/test/verify-signature.js
const { expect } = require("chai")
const { ethers } = require("hardhat")
describe("VerifySignature", function () {
it("Check signature", async function () {
const accounts = await ethers.getSigners(2)
const VerifySignature = await ethers.getContractFactory("VerifySignature")
const contract = await VerifySignature.deploy()
await contract.deployed()
// const PRIV_KEY = "0x..."
// const signer = new ethers.Wallet(PRIV_KEY)
const signer = accounts[0]
const to = accounts[1].address
const amount = 999
const message = "Hello"
const nonce = 123
const hash = await contract.getMessageHash(to, amount, message, nonce)
const sig = await signer.signMessage(ethers.utils.arrayify(hash))
const ethHash = await contract.getEthSignedMessageHash(hash)
console.log("signer ", signer.address)
console.log("recovered signer", await contract.recoverSigner(ethHash, sig))
// Correct signature and message returns true
expect(
await contract.verify(signer.address, to, amount, message, nonce, sig)
).to.equal(true)
// Incorrect message returns false
expect(
await contract.verify(signer.address, to, amount + 1, message, nonce, sig)
).to.equal(false)
})
})
오픈제플린 모듈 살펴보기
위의 예제는 이해할때는 좋지만 실제로 사용할때는 open-zepplin을 사용하는 것이 좋다.
오픈제플린에서도 서명 관련 라이브러리들이 많으니 사용하면 좋다.
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/SignatureChecker.sol
https://medium.com/@ajaotosinserah/mastering-libraries-in-solidity-36d783409dfc
라이브러리 사용할때는 위의 블로그를 참고해서 라이브러리를 활용하면 더 좋다. 내가 참고한 대부분의 코드는 internal호출을 써서 그냥 improt만 해온 후에 ECDSA.recover 이런식으로 사용하였지만 bytes 타입에 using을 쓰면 더 간결하게도 작성이 가능하다. 이런 라이브러리들은 버전에 따라 내용이나 함수등이 다르므로 버전도 잘 기록하고 사용해야 한다.
정리
다음은 여러 라이브러리들을 보고 정리한 사항들이다.
1. 주석이나 설명 등등 살펴보면 대부분 EIP191 서명이다. (EOA에서 서명하는 경우)
2. 서명 검증의 문제로 원문을 그대로 서명하는 대신 보통 해시를 취한 상태에서 서명을 진행한다. (오픈제플린 주석을 포함한 다양한 예제에 써져있음)
3. 오픈제플린의 ECDSA.sol 을 쓰면 거의 다 사용이 가능하지만 컨트렉트 형태의 서명 표준 이나 EIP712서명도 쉽게 해주는 오픈제플린 라이브러리들도 존재한다. 만약 ECDSA를 사용한다면 191로 바꾸어주는 MessageHashUtils를 사용하면 된다.
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
4. 기획할때 서명 타입을 모르거나 안알려주면 eip191서명을 생각하고 있다고 보면 된다. ethers에서 Signer를 써서 sign하는 함수도 eip191서명이다.
오픈제플린을 사용하여 만든 서명 검증 로직
이를 이용해서 컨트렉트 서명은 다음과 같이 검증한다.
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
contract EXAMPLE is {
using ECDSA for bytes32;
using MessageHashUtils for bytes32;
.
.
.
function _verifySignature(
address holder,
bytes32 _hash,
bytes memory signature
) public pure returns (bool) {
if (_hash.toEthSignedMessageHash().recover(signature) != holder) return false;
else return true;
}
function getMessageHash(string memory _message) public pure returns (bytes32) {
return keccak256(bytes(_message));
}
}
해당 코드에 input 값을 넣을때는 아래와 같이 값을 만들어서 넣어준다.
const hash = ethers.keccak256(ethers.toUtf8Bytes(inputString));
const signature = owner.signMessage(ethers.getBytes(hash));
위의 해시의 경우에는 getMessageHash를 사용하거나 위 코드의 ethers로 해시를 만들면 된다.
'블록체인 > Solidity' 카테고리의 다른 글
Maker DAO Multicall , Multicall 설명 (0) | 2024.09.12 |
---|---|
Solidity 내부에서의 string과 bytes의 형변환 시 나는 오류 (0) | 2024.08.23 |
Tally와 호환되는 DAO 컨트렉트 (timelock & erc20) 구현하기 (0) | 2024.08.09 |
다양한 체인에 동일한 주소로 배포가 가능한 Create3 자세히 살펴보기 (1) | 2024.07.02 |
[솔리디티 오버로딩 함수 호출 방법] & 솔리디티 오버로딩에 대한 에러Solidity TypeError: ambiguous function description (i.e. matches 및 해결 방안 (0) | 2024.05.07 |