Overview
의의
지갑 주소에 대한 ownership을 확인한다 (인증).
사용법 ( ethers.js v6 (6.7.1) 기준)
1. 서명하기
import { ethers } from "ethers";
const privateKey =
"0x1723c625260310662cfce151dfee1c6b45441ec29c84e709b9179c5994ec06ea";
const wallet = new ethers.Wallet(privateKey);
const plainMessage = "Hello SignMessage";
const signature = await wallet.signMessage(plainMessage);
console.dir(signature);
2. 인증하기
import { ethers } from "ethers";
const privateKey =
"0x1723c625260310662cfce151dfee1c6b45441ec29c84e709b9179c5994ec06ea";
const wallet = new ethers.Wallet(privateKey);
const plainMessage = "Hello SignMessage";
const signature = await wallet.signMessage(plainMessage);
const walletAddress = ethers.verifyMessage(plainMessage, signature);
console.log("is signature not tampered? :: ", walletAddress === wallet.address);
Deep-dive
BaseWallet.signMessage,
단순히 비동기 함수로 감싸주는 함수이므로 바로 BaseWallet.signMessageSync로 넘어가자.
signMessageSync의 경우, SigningKey.sign 실행시키는게 끝이다.
SigningKey.sign이 실질적으로 ethers에서 제공하는 signature의 가장 깊은 레벨이다.
훝어보면 secp256k1 모듈에서 제공하는 시그니처 생성 알고리즘을 통해, 시그니처 생성 후 단순히 포맷팅해서 리턴한다.
SigningKey.sign의 상위 스택에선 message를 hashMessage함수 실행을 통해 얻은 값을 digest 값으로 보내주는 데, 잠시, hashMessage의 구현을 보고 가자.
message에 MessagePrefix("\x19Ethereum Signed Message:\n") + message.length를 접두에 붙이고 keccak256 digest를 실행한다.
실제론 prefix, 원문 길이 , 원문을 전부 UIntArray 형태로 변환한 후 keccak256을 돌린다.
다시 SigningKey.sign가 어떻게 이뤄지는지 보자.
import * as secp256k1 from "@noble/secp256k1";
import { sha256 } from "@noble/hashes/sha256";
import { hmac } from "@noble/hashes/hmac";
const privateKey =
"0x1723c625260310662cfce151dfee1c6b45441ec29c84e709b9179c5994ec06ea";
const plainMessage = "Hello SignMessage";
secp256k1.etc.hmacSha256Sync = (k, ...m) =>
hmac(sha256, k, secp256k1.etc.concatBytes(...m));
const signature = secp256k1.sign(
getBytesCopy(sha256(plainMessage)),
getBytesCopy(privateKey),
{
lowS: true,
}
);
console.log(signature);
// 위 코드가 전부 SigningKey.sign에 대응
** ethers.js가 의존하고 있는 @noble/secp256k1이 v1 버전(1.7.1)이라, v2로 테스트 했다 **
아래는 바뀐점
오.. 뭔가 자주 익숙한 프로퍼티들이 보인다.
- r : 퍼블릭 키의 x좌표
- s : 시그니처 증명(signature proof) 값
- recovery: 복구 비트 ( ethers에선, v값으로 숫자 28 혹은 27이다)
시그니처 생성 알고리즘 (secp256k1) 3분컷 훝어보기
SECP256k1 훝어보기
SECP256k1은, ECDSA(타원곡선 디지털 서명 알고리즘)의 한 종류로, 특정한 타원곡선을 사용하는 알고리즘이다. 방정식은 아래와같다. y^2 = x^3 + 7 먼저 알고리즘에 대해 이해하려면, 타원곡선의 덧셈
dev-dovi.tistory.com
마지막으로 ethers 자체 클래스인 Signature로 포맷팅만 마무리 하면 우리가 아는 서명값이 된다.
//r ,s , v 순서로 각 값을 헥사 스트링으로 변환하여 접합한것
get serialized(): string {
return concat([ this.r, this.s, (this.yParity ? "0x1c": "0x1b") ]);
}
verifyMessage의 경우, ethers 레벨에선 거의 포맷팅에 관한 로직이라 별 내용이 없다.
사실 verifyMessage라기 보다는, signing에 사용된 address를 복구한단 점에서, 개인적으로는 recoverAddress가 더 맞는 표현인것 같다..
최종적으로, secp256k1 에서 제공하는 recoverPublicKey함수 시행을 통해 public key를 복구하고, public key 해싱을 통해 원래 주소를 복구한다.
개인적인 궁금증들
(좀 글이 길어지는 항목들이 있어서.. 몇몇 항목들은 다른 페이지에 작성하였습니다)
1. 왜 sync함수를 async로 감쌀까?
-> signMessage의 경우 signMessageSync란 함수를 하위 스택으로 실행한다. 이런 패턴이 자주보이긴 했어서, 그동안 2가지 경우로 추측하고 있었는데,
1) 혹시 모를 미래에 비동기 로직이 추가될 수 있으니
2) Blocking 방지
stack overflow 글들 찾아보니 종합해보면 위 두가지 경우 말고는 없었다.
2. Digest란 무엇인가
-> 일반적으로 hashing의 결과물을 digest라 표현한다.
hash function 대신 digest function이라 표현하기도 한다.
3. serializing
-> 데이터를 버퍼 형태로 변환하는 과정을 serializing으로 이해하고 있었는데, 정확히는 데이터를 다른 환경에서 재구성할 수 있는 포맷으로 만드는것이 핵심이었다. 객체의 경우, serializing 대신, marshaling으로 쓴다고도 한다.
4. verifying은 어떻게 동작할까?
먼저 r , s , v는 다음과 같이 정의된다.
- r ( 공개키의 x좌표 )
- v ( 공개키 복구용 보조 팩터 )
단, v의 경우, ethers.js에서 recid 가 항상 참이므로, 28이다.
ECDSA에서 제공하는 복구 알고리즘이 꽤 여러가지 있는데, 대표적인 방법은 아래와 같다.
여기서 s1을 s의 역원의 모듈로 값이라 하자.
SEC1: ECC에서 제시된 포인트 복구 방정식을 시행한다.
위 방정식을 가독성있게 작성하면 아래와 같다. (h는 해시된 평문, G는 genereator point )
R' = (h * s1) * G + (r * s1) * pubKey
이제 위에서 정의한 s1 , 그리고 public Key = G * privateKey라는 점에서 대입을 하면 다음과 같다.
최종적으로 R'.x === r 과 같은지 확인하면 된다.
ethers.js에서는 Public Key Recovery Operation 방식으로 복구한다. (Self-Signed시, 유용하다 명시되어있다.)
과정 자체는, 위의 알고리즘과 동일한 부분이 많아서 'v'의 역할에 대해 이야기해보려한다.
위에서 언급했듯 말그대로 복구 '보조' 팩터인데, 아래 그래프를 보면 이해하기 쉽다.
r값은 기본적으로 공개키의 x좌표인데, 서명에 있어서는, 정확한 좌표값이 요구된다.
왜 v의 역할이 '보조'냐면, 사실 x 좌표에 대응하는 모든 좌표를 대조하면되는데, v가 추가됨으로써 확인 속도를 높여준다.
위 그래프 상에는 2점밖에 없지만, 사실 최대 4개의 후보군이 생성될 수 있는데 이유인즉슨,
r값의 경우, 구할 때, 모듈로 n연산이 들어가고, 모듈로 연산 이전 x 좌표 또한 유효한 좌표일 수 있기에, 사실상 4개가 최대 후보군이다.
따라서, v값은 0 ~ 3의 값을 갖을 수 있는데 (공개키 압축 여부 포함 시, 0~8), 이를 ECDSA에선, recid라 표현한다.
ethers.js에선 recid + 27을 v값으로 삼는데, 버전 바이트...? 로 다른 사람들도 추측하고 있는데 정확한 이유는 모르겠다.
6. PublicKey 와 Address의 차이는 무엇일까?
** Reference **
https://cryptobook.nakov.com/digital-signatures/ecdsa-sign-verify-messages
https://bitcoin.stackexchange.com/questions/38351/ecdsa-v-r-s-what-is-v
ECDSA: (v, r, s), what is v?
Deterministically signing a Tx with RFC6979 returns v, r, s, where r and s are the 2 values used in standard ECDSA signatures. v = 27 + (y % 2), so 27 + the parity of r, as pybitcointools indicates...
bitcoin.stackexchange.com
https://medium.com/mycrypto/the-magic-of-digital-signatures-on-ethereum-98fe184dc9c7
'blockchain' 카테고리의 다른 글
[Cosmos + JS] ADR-036 구현하기 (1) | 2023.10.09 |
---|---|
[BIP32] HD지갑(Hierarchical Deterministic Wallet) 훝어보기 (1) | 2023.10.03 |
[Ethereum] Message prefix용도 (2) | 2023.10.02 |
[Crypto] Signature Replay Attack (0) | 2023.10.02 |
SECP256k1 훝어보기 (0) | 2023.09.28 |