Signing and verifying in ethereum
이더리움에서 eth 를 송금하거나 스마트 컨트랙을 부르는 등의 transaction 을 일으키기 위해서는 signing 이 필요하며, 블록체인 상에서 사용자를 인증하거나 메세지의 유효성을 검증하기 위해 signing 과 validation 이 사용된다.
가령, Alice 가 Bob 에게 어떤 메세지를 보내는 상황을 전제해 보자. 여기서 Bob 이 메세지를 받았다면, 인터넷 네트워크에서는 누구든지 메세지를 보낼 수 있고, 나쁜 의도를 가진 다수의 사용자들이 존재하기 때문에, 진짜 그 메세지가 Alice 로 부터 온 것인지 또, 메세지의 내용이 Alice 가 보낸 그 내용이 맞는지에 대해 의문을 품을 것이다. 이 때문에 블록체인 에서는 signing 의 개념이 존재하며 여기서 그 유효성을 검증하고자 하는 대상을 message 라고 한다.
그렇다면 이제 어떻게 signging 과 validation 이 진행되는 지를 알아보자.
먼저 위 사례에서 Alice 는 자신의 Private Key 로 해당 메세지에 서명을 한다. 이더리움에서 제공하는 web3 를 통해 다음과 같은 간략한 명령을 통해 이런 signing 이 진행되게 된다.
1 | web3.eth.accounts.sign(message, privateKey); |
위 에서 message 는 유효성을 검증하기 위한 메세지로 String
타입이다.
위 과정을 거치면 사용자가 서명을 한 메세지를 의미하는 Signature Object 가 나오게 되는데 그 형식은 다음과 같다.
1 | { |
위에서 알 수 있듯이 Signature 에는 사용자의 원래 메세지 뿐만 아니라 messageHash 라는 "\x19Ethereum Signed Message:\n" + message.length + message
의 양식으로 포맷팅된 후 keccak256(SHA3) 으로 해쉬된 해시값을 비롯하여 signature 를 구성하는 v, r, s 와 실제 서명인 Signature 가 포함된다. 여기서 r 과 s 는 각각 Signature의 첫 32byte 와 뒤 32byte 를 나타낸다.
* 위의 MessageHash 는 web3 가 제공하는 hashMessage(message) 함수 호출을 통해서도 얻을 수 있다.
위처럼 sign 을 완료했다면, 해당 Signature 를 받은 사용자는 다음과 같은 recover 함수 호출을 통해 올바른 사용자인지를 판별 할 수 있다.
example
1 | web3.eth.accounts.recover(signatureObject); |
EIP712
What is EIP712
EIP 712 란 Ethereum Improvement Proposals 712 의 약어로 이더리움에서 향후 지원하게 될 다양한 제안들중 하나이다. 과거 사용자가 어떤 거래를 함에 있어 sign 을 할때에는 sign 의 대상이 되는 message 가 hash 화 되어 존재하기 때문에, 서명을 하는 사용자가 자신이 서명하는 내용에 대해 잘 알기 힘든 문제가 있었으며, 가령 유사한 해시값을 가진 피싱 사이트로 유도하여 사이닝을 유도한다던가 하는 다양한 위험에 노출되어 있었다.
이를 해결하기 위해 안전하고 값이 변조되지 않는 해싱을 보장하면서도 readability 를 가질 수 있는 서명방법을 고안하게 되었으며 그 제안내용이 EIP712 에 제안되었다.
EIP712 를 통해 사용자는 자신이 서명하는 정보에 대해 명확하게 인지할 수 있게 된다고 볼 수 있다.
가령, 위에서처럼 일반적인 signing의 과정에서는 sign 을 요청하는 사람이 제공하는 message값(주로 hash값) 만을 보고 사용자가 sign 을 진행하지만 EIP712 에서는 json 데이터를 보고 자신이 서명하는 데이터를 명확히 알 수 있다.
다음은 EIP712 를 통해 수행되는 서명의 방법을 나타낸다.
먼저 서명을 하고자 하는 데이터의 형태를 정의하는데 이러한 typed structured data
를 먼저 정의하는 것으로 서명이 시작된다. 아래와 같이 서명을 하고자 하는 메세지를 json 형태로 정의를 한다. 서명자는 아래와 같은 데이터를 통해 어떤 내용에 자신이 서명을 하는지 알 수 있다.(아래의 메세지는 meta mask 등의 지갑 어플리케이션에서 파싱되어 보여지며, EIP712 는 이러한 지갑 어플리케이션에서 제공하는 기능을 말미암에 제공된다.)
컨트랙트 도메인 타입의 정의
1 | const domain = [ |
1 | var message = { |
위처럼 서명하고자 하는 message 를 정의하였다면, 해당 메세지가 어떤 도메인 사업자(즉 DAPP 사업자로 이해할 수 있다) 에서부터 온 메세지인지를 나타내는 구분자인 Domain Separator 를 정의해 준다.
1 | const domainData = { |
name: DAPP 혹은 Protocol 의 이름
version: DAPP 혹은 Platform version
chainId: 테스트 넷인지 메인넷인지 등을 구분하는 chain id 로 EIP155 에서 제안되었다.
verifyingContract: 해당 signature 를 verify 할 스마트 컨트랙트의 주소
salt: 32 바이트의 hard code 된 유니크한 값으로 컨트랙트와 DAPP 사이에 공유되어 다른 DAPP 과 구분되는 최후의 보루이다.
message type 과 Domain separator 가 정의되었다면 다음과 같이 Json 형태의 data 를 만들고 이를 stringily 시켜 sign 을 요청한다.
1 | const data = JSON.stringify({ |
data 내의 types
필드는 스마트 컨트랙트 내에서의 데이터 구조를 나타내며 반드시 struct name
과 정확히 일치해야한다. 또한 PrimaryType
은 데이터 구조에서의 최상위 자료 구조형이 무엇인지 명시한다.
Validation in Smart contract
클라이언트에서 signing 을 위해 formatting 과 hashing 을 거친 것처럼 같은 내용의 코드가 Smart Contract 에도 포함되어야 한다. 이 과정을 통해 ecrecover
함수를 통해 해당 서명에 사인한 account 의 address 를 알 수 있다.
인증을 위한 Contract 를 위해 제일 먼저 EIP712 에서 앞서 정의한 data type 을 struct 로 정의해야 한다.
1 | struct Identity { |
그 다음으로는 위 data structure 에 맞는 type hash
를 정의해야 하며 그 코드는 다음과 같다.
1 | string private constant IDENTITY_TYPE = "Identity(uint256 userId,address wallet)"; |
여기서 comma 와 bracket 사이에 공백이 들어가지 않는것을 유념하자. 또한, parameter 의 이름과 자료형이 클라이언트의 자료형과 변수명과 완벽하게 일치해야 한다.
또한 다음과 같이 Domain Separator
도 다음과 같이 hashify 되어야 한다.
1 | uint256 constant chainId = 1; |
아래와 같이 각 data 를 hashify 한다.
1 | function hashIdentity(Identity identity) private pure returns (bytes32) { |
마지막으로 다음과 같이 signature 를 verify 하는 함수를 작성해 준다.
1 | function verify(address signer, Bid memory bid, sigR, sigS, sigV) public pure returns (bool) { |
Keccak256 hashing
이더리움에서는 SHA3 해싱을 위해 keccak256 을 사용한다.