본문 바로가기

Blockchain/Blockchain In Action

[Blockchain In Action] 5장 보안과 프라이버시

Blockchain In Action 5장


블록체인 인 액션 책을 읽고 공부한 내용을 기록한 글 입니다. 글에 나와있는 내용과 사진은 모두 블록체인 인 액션에 포함된 내용 혹은 이를 정리한 것 입니다. 문제가 될 시 삭제하겠습니다.


모든 시스템은 보안과 프라이버시 문제를 가지고 있고, 블록체인 기반 시스템에서는 특히 그러하다.

블록체인 시스템은 중앙화된 주체가 제공하는 신뢰의 경계를 넘어서서 작동해야 하기 때문이다.

탈중앙화 시스템에서 참여자들은 분산되어 있고, 고유의 asset을 가지고 있으며, 원할 때 가입하거나 탈퇴가 가능하고, 신뢰 layer로 블록체인에 의존한다.

탈중앙화 시스템에서는 이러한 이슈에 대처하기 위한 암호학과 해싱 기법을 사용한다.

암호학은 탈중앙화 참여자를 위한 아이덴티티로 쓸 수 있는 어카운트 주소를 만들어내는 핵심적인 역할을 한다.

해싱 기법은 보안과 프라이버시를 구현할 때 사용된다.

대부분의 일상 프로그래밍 프로젝트에서 보안은 묵시적이나, 블록체인 기반 솔루션에서 암호학은 필수 불가결하고 명시적이다.

탈중앙화 블록체인 기반 솔루션에서 암호학은 아래의 네 가지 역할을 담당한다.

- 참여자와 다른 엔티티를 위한 디지털 아이덴티티 생성
- 데이터와 드랜잭션의 보안 보장 
- 데이터의 프라이버스를 확립
- 문서에 디지털 서명을 함

비대칭키 암호학

대칭키 암호학은 탈중앙화 애플리케이션(Dapp)에 적합하지 않다. 대칭키 암호학은 암호화하고 이것을 푸는 과정에 같은 키를 사용하기 때문이다.

대칭키 방식의 이슈중 하나는 키 분배로서, 모든 참여자에게 어떻게 키를 비밀리에 전달할 수 있는가 하는 문제다.

만일 이 키를 공개해버리면 누구나 메시지를 해독할 수 있기 때문이다. 탈중앙화 시스템에서는 이 이슈가 더욱 심각해진다.

이러한 이유로 인해 블록체인 시스템에서는 비대칭키 암호학을 사용한다.

비대칭키 암호학은 일반적으로 공개키 암호학으로 알려져있다. 하나의 비밀키 대신에 두 개의 다른 키를 사용하는 방식이다.

공개키는 오픈하고 개인 키는 암호를 사용해 별도로 보관하면서, 각 참여자가 공개키를 사용해 메시지를 암호화하고, 상대편은 자신의 개인 키를 사용해 암호화된 메시지를 해독하는 것이다.


이더리움에서의 비대칭키 암호학

이더움에서의 비대칭키 암호학 메커니즘은 다음과 같다 .

1. 256비트의 난수를 생성하여 개인키로 설정

2. 타원 곡선 암호학 알고리즘이라는 특수한 알고리즘을 개인키에 적용해서 고유한 공개 키를 추출한다.

개인키는 패스워드로 보호를 받고, 공개키는 외부에 공개된다.

3. 해시 함수를 공개키에 적용해서 어카운트 주소를 얻는다.

a. 이 주소는 공개키보다 짧은 20바이트(160비트)이다.
b. 주소는 읽기 쉽도롤 16진수로 표시되며, 앞에 0x를 붙여 16진수임을 나타낸다.

롭스텐

롭스텐은 이더리움 블록체인 프로토콜을 구현한 퍼블릭 테스트 네트워크이고, 테스트 이더를 사용한다.

Remix와 다른 테스트 네트워크(ex. 가나쉬)에서 초기 테스트를 끝낸 후에 배포 실험을 해보기 좋다.

테스트를 위해 다음의 환경 셋업이 필요하다.

  • 어카운트를 관리하고 트랜잭션을 사인하기 위한 지갑 = 메타마스크
  • 미리 정해진 테스트 어카운트 주소를 메타마스크에 설치.
  • 롭스텐 이더 배포소, Tx 실행과 피어 참여자 간의 전송에 필요한 이더를 제공해줘야 한다.
  • 리믹스의 web3 환경. 메타마스크 유저 인터페이스와 연결되어 롭스텐 주소를 가지고 스마트 컨트랙트와 상호작용을 하게 해준다.
  • 롭스텐 네트워크에 배포될 스마트 컨트랙트.

니모닉 양식의 개인키 사용하기

어카운트 주소를 생성하고 다시 찾으려면 매번 개인키가 필요한데, 이 개인키를 외우는 것은 사실상 불가능하다.

대신 개인키를 나타내는 니모닉을 사용하자.

BIP39, 12단어 니모닉 생성해주는 사이트 Link


배포

니모닉 키를 이용해서 계정을 만들어낸 후, Ropsten site에 가서 해당 어카운트 주소를 입력하면 1 테스트 이더를 받을 수 있다.

Remix에 미리 만들어두었던 Counter.sol 파일을 복사한 후에,

Remix의 환경을 Injected Web3로 바꾸면 메타마스크의 어카운트 넘버가 보인다.

Remix IDE를 이용해 간단한 컨트랙트를 Deploy하면 롭스텐 이더스캔에서 우리가 만들어낸 트랜잭션을 확인할 수 있다.

이제 Remix가 제공하는 UI를 통해 스마트 컨트랙트와 상호작용을 해보면, 매 트랜잭션마다 메타마스크에서 컨펌을 하는 것을 볼 수 있다.

이 컨트랙트는 이제 롭스텐에 있는 모든 참여자들에게 오픈되어 있으며, 더 이상 프라이빗하지 않다는 점을 상기해야 한다.


해싱 기초

해싱은 임의의 크기를 가진 데이터를 표준화한 고정 크기의 값으로 매핑하는 것이다.

해싱 함수는 특수하게 정의되어 임의의 길이를 가진 데이터를 고정된 길이를 가진 데이터로 매핑해주는 프로세스를 말한다.

데이터 요소에 1비트 내용만 바뀌어도 전체 내용이 달라지기 때문에, 어떠한 데이터도 고정된 길이의 해시로 간결하고 충돌 가능성이 없게 표현 가능하다.

충돌 가능성이 없다는 것은 서로 다른 데이터 요소에 해시 함수를 적용해 생성된 두 개의 해시가 같은 값을 가질 확률은 거의 없다는 말이며,

같은 데이터 요소에 해시 함수를 적용할 때 항상 같은 고유한 해시를 얻을 수 있다는 것을 의미한다.


솔리디티 해싱함수

솔리디티는 SHA256, Keccak, RIPEMD160이라는 세 가지 해싱 함수를 제공한다.

해싱을 위한 간단한 스마트 컨트랙트 예제는 다음과 같다.

pragma solidity >=0.4.22 <= 0.6.0;

contract Khash {

bytes32 public hashedValue;
      function hashMe(uint value1, bytes32 password) public {
      hashedValue = keccak256(abi.encodedPacked(value1, password));
    }
}

abi.encodedPacked 함수는 파라미터들을 묶어서 여러 파라미터의 바이트 값을 리턴해주며, keccak256 함수는 이 바이트 값의 해시를 계산해준다.


해싱 App

Dapp에서 프라이버시와 보안을 지켜주는 Secure Hashing에 대해서 알아보자. 이를 위해 블라인드 경매 예제를 다음과 같이 설정한다.

- 어떤 예술 작품을 블라인드 경매로 판매한다.
- 여러 작품을 경매로 판매할 수 있겠지만, 여기서는 한 작품만 판매한다는 가정이다.
- 수혜자는 경매의 여러 단계 {Init, Bidding, Reveal, Done}을 관리한다. 
- 경매가 시작하고나면 입찰자는 한 번에 하나씩 입찰을 한다. (Bidding Phase)
- 입찰은 보안이 보장되며 프라이빗하게 이루어진다. (수혜자 또한 볼 수 없다)
- 일정 시간 후, 수혜자는 Reveal Phase로 진행한다. 
- 입찰자는 각자의 입찰 내용을 공개하고, 수혜자는 전체 입찰 내용을 공개해서 가장 높은 가격을 제시한 입찰자와 입찰가를 찾아낸다.
- 수혜자는 Done Phase로 변경하며 경매를 종료하고, 수혜자의 어카운트로 최고가 입찰가를 전송한다. 
- 경매에 떨어진 입찰자는 예치금을 출금할 수 있으나 낙찰받은 입찰자는 금액을 출금할 수 없다.

블라인드 경매 예제는 두 가지의 주요 규칙을 가지고 있는데, 수혜자가 경매의 시작, 종료, Bidding, Reveal 단계 시점을 결정하고,

오직 수혜자만이 한 단계에서 다음 단계로 변화시킬 수 있다는 것이다.

예제 코드

pragma solidity >=0.4.22 <=0.6.0;

contract BlindAuction {

    struct Bid {                   
        bytes32 blindedBid;
        uint deposit;
    }

    // state will be set by beneficiary  
    enum Phase {Init, Bidding, Reveal, Done}  
    Phase public state = Phase.Init; 

    address payable beneficiary; //owner  
    mapping(address => Bid) bids;  


    address public highestBidder; 
    uint public highestBid = 0;   

    mapping(address => uint) depositReturns; 
    //modifiers
    modifier validPhase(Phase reqPhase) 
    { require(state == reqPhase); 
      _; 
    } 

   modifier onlyBeneficiary()
   { require(msg.sender == beneficiary); 
      _;
   }

constructor(  ) public {    
        beneficiary = msg.sender;
        state = Phase.Bidding;
    }

    function changeState(Phase x) public onlyBeneficiary {
        if (x < state ) revert();
        state = x;
    }

    function bid(bytes32 blindBid) public payable validPhase(Phase.Bidding)
    {  
        bids[msg.sender] = Bid({
            blindedBid: blindBid,
            deposit: msg.value
        });
    }

    function reveal(uint value, bytes32 secret) public   validPhase(Phase.Reveal){
        uint refund = 0;
            Bid storage bidToCheck = bids[msg.sender];
            if (bidToCheck.blindedBid == keccak256(abi.encodePacked(value, secret))) {
            refund += bidToCheck.deposit;
            if (bidToCheck.deposit >= value) {
                if (placeBid(msg.sender, value))
                    refund -= value;
            }}

        msg.sender.transfer(refund);
    }

    function placeBid(address bidder, uint value) internal 
            returns (bool success)
    {
        if (value <= highestBid) {
            return false;
        }
        if (highestBidder != address(0)) {
            // Refund the previously highest bidder.
            depositReturns[highestBidder] += highestBid;
        }
        highestBid = value;
        highestBidder = bidder;
        return true;
    }


    // Withdraw a non-winning bid
    function withdraw() public {   
        uint amount = depositReturns[msg.sender];
        require (amount > 0);
        depositReturns[msg.sender] = 0;
        msg.sender.transfer(amount);
        }


    //End the auction and send the highest bid to the beneficiary.
    function auctionEnd() public  validPhase(Phase.Done) 
    {
        beneficiary.transfer(highestBid);
    }
}