blockchain

Signing and verifying in ethereum

이더리움에서 eth 를 송금하거나 스마트 컨트랙을 부르는 등의 transaction 을 일으키기 위해서는 signing 이 필요하며, 블록체인 상에서 사용자를 인증하거나 메세지의 유효성을 검증하기 위해 signing 과 validation 이 사용된다.

가령, Alice 가 Bob 에게 어떤 메세지를 보내는 상황을 전제해 보자. 여기서 Bob 이 메세지를 받았다면, 인터넷 네트워크에서는 누구든지 메세지를 보낼 수 있고, 나쁜 의도를 가진 다수의 사용자들이 존재하기 때문에, 진짜 그 메세지가 Alice 로 부터 온 것인지 또, 메세지의 내용이 Alice 가 보낸 그 내용이 맞는지에 대해 의문을 품을 것이다. 이 때문에 블록체인 에서는 signing 의 개념이 존재하며 여기서 그 유효성을 검증하고자 하는 대상을 message 라고 한다.

그렇다면 이제 어떻게 signgingvalidation 이 진행되는 지를 알아보자.

먼저 위 사례에서 Alice 는 자신의 Private Key 로 해당 메세지에 서명을 한다. 이더리움에서 제공하는 web3 를 통해 다음과 같은 간략한 명령을 통해 이런 signing 이 진행되게 된다.

1
web3.eth.accounts.sign(message, privateKey);

위 에서 message 는 유효성을 검증하기 위한 메세지로 String 타입이다.

위 과정을 거치면 사용자가 서명을 한 메세지를 의미하는 Signature Object 가 나오게 되는데 그 형식은 다음과 같다.

1
2
3
4
5
6
7
8
9
{
message: 'Some data',
messageHash: '0x1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655',
v: '0x1c',
r: '0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd',
s: '0x6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029',
signature: '0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f\
5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c'
}

위에서 알 수 있듯이 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
web3.eth.accounts.recover(signatureObject);
web3.eth.accounts.recover(message, signature [, preFixed]);
web3.eth.accounts.recover(message, v, r, s [, preFixed]);

web3.eth.accounts.recover({
messageHash: '0x1da44b586eb0729ff70a73c326926f6ed5a25f5b056e7f47fbc6e58d86871655',
v: '0x1c',
r: '0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd',
s: '0x6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029'
})
> "0x2c7536E3605D9C16a7a3D7b1898e529396a65c23"

// message, signature
web3.eth.accounts.recover('Some data', '0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c');
> "0x2c7536E3605D9C16a7a3D7b1898e529396a65c23"

// message, v, r, s
web3.eth.accounts.recover('Some data', '0x1c', '0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd', '0x6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029');
> "0x2c7536E3605D9C16a7a3D7b1898e529396a65c23"

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
{ name: "salt", type: "bytes32" },
];

const bid = [
{ name: "amount", type: "uint256" },
{ name: "bidder", type: "Identity" },
];

const identity = [
{ name: "userId", type: "uint256" },
{ name: "wallet", type: "address" },
];
1
2
3
4
5
6
7
var message = {
amount: 100,
bidder: {
userId: 323,
wallet: "0x3333333333333333333333333333333333333333"
}
};

위처럼 서명하고자 하는 message 를 정의하였다면, 해당 메세지가 어떤 도메인 사업자(즉 DAPP 사업자로 이해할 수 있다) 에서부터 온 메세지인지를 나타내는 구분자인 Domain Separator 를 정의해 준다.

1
2
3
4
5
6
7
const domainData = {
name: "My amazing dApp",
version: "2",
chainId: parseInt(web3.version.network, 10),
verifyingContract: "0x1C56346CD2A2Bf3202F771f50d3D14a367B48070",
salt: "0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558"
};

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
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
const data = JSON.stringify({
types: {
EIP712Domain: domain,
Bid: bid,
Identity: identity,
},
domain: domainData,
primaryType: "Bid",
message: message
});

web3.currentProvider.sendAsync(
{
method: "eth_signTypedData_v3",
params: [signer, data],
from: signer
},
function(err, result) {
if (err) {
return console.error(err);
}
const signature = result.result.substring(2);
const r = "0x" + signature.substring(0, 64);
const s = "0x" + signature.substring(64, 128);
const v = parseInt(signature.substring(128, 130), 16);
// The signature is now comprised of r, s, and v.
}
);

data 내의 types 필드는 스마트 컨트랙트 내에서의 데이터 구조를 나타내며 반드시 struct name 과 정확히 일치해야한다. 또한 PrimaryType 은 데이터 구조에서의 최상위 자료 구조형이 무엇인지 명시한다.

Validation in Smart contract

클라이언트에서 signing 을 위해 formatting 과 hashing 을 거친 것처럼 같은 내용의 코드가 Smart Contract 에도 포함되어야 한다. 이 과정을 통해 ecrecover 함수를 통해 해당 서명에 사인한 account 의 address 를 알 수 있다.

인증을 위한 Contract 를 위해 제일 먼저 EIP712 에서 앞서 정의한 data type 을 struct 로 정의해야 한다.

1
2
3
4
5
6
7
8
9
struct Identity {
uint256 userId;
address wallet;
}

struct Bid {
uint256 amount;
Identity bidder;
}

그 다음으로는 위 data structure 에 맞는 type hash 를 정의해야 하며 그 코드는 다음과 같다.

1
2
string private constant IDENTITY_TYPE = "Identity(uint256 userId,address wallet)";
string private constant BID_TYPE = "Bid(uint256 amount,Identity bidder)Identity(uint256 userId,address wallet)";

여기서 comma 와 bracket 사이에 공백이 들어가지 않는것을 유념하자. 또한, parameter 의 이름과 자료형이 클라이언트의 자료형과 변수명과 완벽하게 일치해야 한다.

또한 다음과 같이 Domain Separator 도 다음과 같이 hashify 되어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
uint256 constant chainId = 1;
address constant verifyingContract = 0x1C56346CD2A2Bf3202F771f50d3D14a367B48070;
bytes32 constant salt = 0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558;
string private constant EIP712_DOMAIN = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)";
bytes32 private constant DOMAIN_SEPARATOR = keccak256(abi.encode(
EIP712_DOMAIN_TYPEHASH,
keccak256("My amazing dApp"),
keccak256("2"),
chainId,
verifyingContract,
salt
));

아래와 같이 각 data 를 hashify 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hashIdentity(Identity identity) private pure returns (bytes32) {
return keccak256(abi.encode(
IDENTITY_TYPEHASH,
identity.userId,
identity.wallet
));
}

function hashBid(Bid memory bid) private pure returns (bytes32){
return keccak256(abi.encodePacked(
"\\x19\\x01",
DOMAIN_SEPARATOR,
keccak256(abi.encode(
BID_TYPEHASH,
bid.amount,
hashIdentity(bid.bidder)
))
));
}

마지막으로 다음과 같이 signature 를 verify 하는 함수를 작성해 준다.

1
2
3
function verify(address signer, Bid memory bid, sigR, sigS, sigV) public pure returns (bool) {
return signer == ecrecover(hashBid(bid), sigV, sigR, sigS);
}

Keccak256 hashing

이더리움에서는 SHA3 해싱을 위해 keccak256 을 사용한다.

Getting started with ethereum

Kinds of accounts

이더리움에는 두가지 종류의 account 가 있는데, External AccountContract Account 가 그것이다.

먼저 External Account 는 공개키 방식의 계정이며, address 역할을 하는 public keynonce 로 구성이 되며, Contract Account 는 일반적인 smart contract 이다.

여기서 account 와 account 사이에 자금이 이동하면 그것을 transaction 이라고 말한다.

Transaction

이더리움에서 transaction 이란 하나의 EOA 로부터 다른 account 로 전송되는 message 를 포함한 signed data packet 을 의미하며 다음과 같은 정보들을 담고 있으며, EOA 로 부터 EOA 로 가는 경우는 이더를 송금하는 것을 내포하고, 만약 수신자가 Contract Account 라면 해당 contract 가 어떤 코드를 실행하도록 trigger 하는 것이 된다.

  • receipient
  • signature
  • value
  • data: which can contain the message sent to a contract

여기서 data 는 contract 가 실행에 필요한 정보를 들고 있으며 Contract ABI(Application Binary Interface) 에 따라 컨트랙트와 소통한다. 이 ABI 는 외부 와컨트랙트 간의 상호작용에 필요할 뿐만 아니라 contract 간의 상호작용에도 적용된다.

따라서 data 는 다음의 규약에 따라 인코딩 되어 사용되며 ABI(Application Binary Interface 에 따라 정의된다.

함수 선택자

data 의 함수 시그니처의 Keccak-256 (SHA-3) hash 의 첫 4바이트는 어떤 함수를 호출할지를 나타내며 function 시그니처로 부터 도출된다.

1
2
3
4
5
6
7
pragma solidity >=0.4.16 <0.6.0;

contract Foo {
function bar(bytes3[2] memory) public pure {}
function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
function sam(bytes memory, bool, uint[] memory) public pure {}
}

위의 예제에서 baz 함수를 호출하고자 하는 transaction 의 data의 함수선택자는 0xcdcd77c0 인데 이는 method 의 id 로 ASCII format 으로 작성된 function signature 인 baz(uint32,bool 의 Keccak hash(sha3) 의 첫번째 4 바이트이다.

함수 Parameter

또한 해당 함수의 parameter 로 69와 true 를 제공한다면 69를 32바이트 로 패딩한 0x0000000000000000000000000000000000000000000000000000000000000045 가 값이 된다. 0x0000000000000000000000000000000000000000000000000000000000000001

ABI(Application Binary Interface)

컴퓨터 과학에서 흔히 말하는 ABI 는 두가지 종류의 binary 프로그램 모듈 사이의 인터페이스이다. 쉬운 예로는 libraryoperating 시스템의 경우가 있는데, 하나의 라이브러리는 여러 시스템 상에서 동작해야 하므로 해당 소스코드가 다양한 운영체제에서 동작하기 위해서는 약속된 인터페이스가 필요하다. 또 한 예로는 유저에 의해 실행되는 프로그램의 예가 있는데 가령 여러 운영체제에서 해당 프로그램을 실행하여도 동작하기 위해서는 이러한 ABI 가 정의되어 있어야 한다.

이와 같은 맥락에서 이더리움 에서의 ABI 란 특정 함수가 이더리움에서 실행되기 위한 포맷팅의 규약으로써 이해될 수 있다.

아래는 스마트 컨트랙트 내에서 특정 데이터를 ABI 인코딩 하는 것을 보여준다.

1
abi.encode("AAAA")

위 코드는 아래와 같이 3단어로 구성된 96(32byte *3) 사이즈의 바이트 문자열을 리턴한다.

1
2
3
0x0000000000000000000000000000000000000000000000000000000000000020
0x0000000000000000000000000000000000000000000000000000000000000004
0x4141414100000000000000000000000000000000000000000000000000000000

한 줄씩 살펴보면 먼저 첫번째 라인은 해당 문자열의 starting offset(32 in decimal) 를 32byte 문자열에 padding 한 것이며, 두번째 문자열은 인코딩 하는 데이터의 길이인 4를 32바이트 문자열에 padding 하여 나타낸다. 마지막으로 세번째 문자열은 실제 우리의 데이터인 “AAAA” 를 UTF-8 인코딩하여 32 바이트 문자열에 padding 하여 나타내어 준다.

위의 encoding 방식보다 다소 간편하게 인코딩을 하고 싶다면 abi.encodePacked("AAAA") 라는 함수를 사용할 수 있다. 이 함수는 32바이트보다 작은 문자는 그냥 해당 문자열을 바이트로 출력하고 32바이트에 padding 하지도 않는다.

한가지 흥미로운 사실이 있는데, keccak256 을 사용하여 hashing 을 할때 복수개의 인자를 전달하면 이것을 내부적으로 abi.encodePacked 함수로 인코딩 하여 해싱을 한다는 것이다. 현재는 복수개의 인자를 전달하면 warning 을 출력하기는 하지만, 알아두도록 하자.

다음은 Keccak256 에서 abi.encodePacked 를 사용하는 예이다. 아래의 두 hashing 은 정확히 동일한 역할을 수행한다.

1
keccak256("AAAA", "BBBB", 42);
1
keccak256(abi.encodePacked("AAAA", "BBBB", 42));
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×