DID를 활용한 Covid19 백신접종증명서 개발하기

매정·2022년 6월 9일
3
post-thumbnail

🔆 과제 선택 이유

블록체인 실습을 진행하는 과제를 받았다. 다양한 주제들 중 나는 ‘DID를 활용한 백신접종증명서 개발'을 선택하였다. 그 이유는 다음과 같다.

  • DID는 블록체인 기술 중 현재 실생활에서 접목이 많이 되고 있는 기술이며 (특히 백신접종증명서는 전국민이 질리도록 사용했지 않은가?)
  • 해당 과제를 진행하면서 솔리디티 언어를 좀 더 많이 이해할 수 있게 되지 않을까 생각했다.



🔆 백신접종증명 시나리오 설계

진행하기에 앞서,

DID, 검증가능한 크레덴셜 등의 개념을 알고 크레덴셜 발행구조와 그 흐름을 알고 있는 것이 중요하다.

이전에 DID를 공부하면서 정리한 것을 참고하였다.

포스팅 마지막에 일반 DID 기술 플로우에 백신접종증명 사례를 대입해본 절차를 정리해두었는데,

해당 내용을 바탕으로 컨트랙트를 설계해 보았다.

기존 DID 신원절차와 방식을 다음과 같이 단순화 하였다.

  1. 주체는 동일하다. Holder (개인), Issuer (질병관리청), Verifier(다중이용시설) 로 설정하였다.
  2. 모든 Holder들의 Credential을 보관하는 IPFS (분산 저장소) 와, 개인이 자신의 Credential을 저장할 수 있는 Personal Mobile을 설정해주었다.
  3. 순서는 아래와 같으나, 1번: ‘Holder가 Issuer에게 Credential 발급을 요청’은 구현이 복잡해 오프체인에서 진행된다고 가정하였다.



🔆 전체코드

Github: https://github.com/mae-zung/learn_solidity/blob/master/vaccineCredential.sol


// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.10;

contract Covid19VaccineCredential {
    address private issuerAddress;
    uint256 private idCount;
    mapping(uint8 => string) private vaccineType;

    struct Credential{
        uint256 id;
        address issuer;
        uint8 vaccineType;
        string date;
    }

    mapping(address => Credential) private credentials;
    mapping(address=>Credential) public IPFS;
    mapping(address=>Credential) private personalMobile;

    constructor() {
        issuerAddress = msg.sender;
        idCount = 1;
        vaccineType[0] = "Pfizer";
        vaccineType[1] = "Moderna";
        vaccineType[2] = "Astrazeneca";
    }

    // Issuer - 크레덴셜을 발급해줌.
    function issueCredential(address _holderAddress, uint8 _vaccineType, string calldata _date) public returns(bool){
        require(issuerAddress == msg.sender, "Not Issuer");
				Credential storage credential = credentials[_holderAddress];
        require(credential.id == 0);
        credential.id = idCount;
        credential.issuer = msg.sender;
        credential.vaccineType = _vaccineType;
        credential.date = _date;
        
        idCount += 1;

        registerCredential(_holderAddress);

        return true;
    }

    // Holder - 크레덴셜을 받아와서 자신의 모바일에 저장함.
    function getCredential(address _holderAddress) public returns(bool){
        Credential memory mobileStorage = credentials[_holderAddress];
        personalMobile[_holderAddress] = mobileStorage;
        return true;
    }

    

     // Issuer - 발급한 크레덴셜을 IPFS에 등록함.
    function registerCredential (address _holderAddress) internal {
        Credential memory ipfs = credentials[_holderAddress];
        IPFS[_holderAddress]=ipfs ;
    }

    // Holder - 크레덴셜을 위조, 변조함.
    function tamperCredential (address _holderAddress, uint8 _vaccineType, string calldata _date) public{
        Credential storage tampered = personalMobile[_holderAddress];
        tampered.vaccineType=_vaccineType;
        tampered.date=_date;

    }

    function hash(uint256 _id, address _issuer, uint8 _vaccineType, string memory _date) pure internal returns(bytes32) {
     return keccak256(abi.encodePacked(_id,_issuer,_vaccineType,_date));
}

    // Verifier - 제출받은 크레덴셜과 IPFS에 등록된 크레덴셜을 비교함.
    function verifyCredential (address _holderAddress) view public returns(bool){

        Credential memory holderCredential = personalMobile[_holderAddress];
        Credential memory ipfsCredential = IPFS[_holderAddress];

        bytes32 fromHolder = hash(holderCredential.id, holderCredential.issuer, holderCredential.vaccineType, holderCredential.date);
        bytes32 fromIpfs = hash(ipfsCredential.id, ipfsCredential.issuer, ipfsCredential.vaccineType, ipfsCredential.date);
        
        if(fromHolder==fromIpfs){
            return true;
        }else return false;

    }



}



🔆 코드 해설

1. 검증가능한 Credential 구조체

  • id: index 순서
  • issuer: 발급자
  • vaccineType: 백신 타입 (화이자, 모더나, 아스트라제네카)
  • date: 백신을 맞은 날짜

2. 맵핑 타입의 변수 선언

  • credentials: 크리덴셜이 발행될 때 사용하는 변수
  • IPFS: IPFS에 저장되는 크리덴셜
  • personalMobile: 개인의 휴대폰에 저장되는 크리덴셜

개인 공간에 크리덴셜을 저장하는 방법을 위와 같이 단순화하였다.


3. Issuer - issueCredential 함수

issueCredential 함수를 통해 issuer는 holder에게 크리덴셜을 발행해준다. 발행 후 크리덴셜을 IPFS에 저장시켜주는 registerCredential 함수를 실행시킨다.


4. Issuer - registerCredential 함수

registerCredential 함수를 통해 issuer는 ipfs에 발행한 크리덴셜을 등록한다.


5. Holder - getCredential 함수

getCredential 함수를 통해 Holder는 자신의 personal mobile에 크리덴셜을 저장한다.


6. Verifier - verifyCredential 함수, hash 함수

verifyCredential 함수를 통해 Verifier는 holder가 갖고 있는 personal mobile 내의 크리덴셜 값과 공개되어 있는 IPFS에 해당 holder에 대한 크리덴셜 값을 해싱하여 비교한다. 동일할 경우 true를, 다를 경우 false를 반환한다.

Verifier가 검증을 하는 방식은 정말 많고 복잡하다. (참고) 위 방식은 내가 임의로 구현해준 방식이다.

솔리디티에서 해싱은 keccak256을 주로 사용하고, abi.encodePacked는 인코딩을 위해 사용한다. 따라서 우선 크리덴셜 값들을 인코딩하고, 해싱을 해주었다. 만약 데이터가 털끝이라도 바뀐다면 해싱값은 완전히 바뀔 것이다.

인코딩 과정을 거치지 않고 해싱을 바로 진행해도 되지만, 데이터를 보호할 목적이 있다면 지양하는 것이 좋다.

솔리디티에서의 해싱과 인코딩 관련 내용은 이 곳을 참고했다.

7. (option) Holder - tamperCredential 함수

이 함수는 Holder가 자신의 크리덴셜 값을 위, 변조하는 함수다. 백신의 종류와 날짜를 파라미터로 받아서 변경할 수 있도록 하였다.

좀 더 다이나믹한(?) 구현을 위해 추가를 해본 기능이다. 크리덴셜을 검증할 때, 두 값이 항상 같은 경우만 나올 것이라서, 예외 상황을 주고 싶었다.




🔆 동작 과정

컨트랙주소: 0x6c58023f14b627E6a8F36650CD47cA495c57D4CC
네트워크: Rinkeby 테스트 네트워크
이더스캔 주소: https://rinkeby.etherscan.io/address/0x6c58023f14b627E6a8F36650CD47cA495c57D4CC


1. 코드 작성 후 배포

2. issueCredential 함수 실행

이미 holder의 credential 정보가 오프체인에서 검증되고 전달되었다는 가정 하에 진행된다. holder의 주소, 백신 타입(0: 화이자, 1: 모더나, 2: 아스트라제네카), 백신 맞은 날짜를 파라미터로 전달해주어 크리덴셜을 생성한다.


3. IPFS에 크리덴셜이 제대로 저장이 되었는지 확인

issueCredential 함수 실행 시 마지막에 registerCredential 함수가 실행된다. 해당 함수 실행 시 크리덴셜 정보가 공개된 IPFS에 저장되기 때문에 IPFS에 holder의 주소를 인자로 넣으면 크리덴셜을 확인할 수 있다.


4. getCredential 함수 실행

getCredential 함수를 실행시켜 holder가 자신의 personal mobile에 크리덴셜을 저장할 수 있도록 한다. personal mobile은 private이기 때문에 크리덴셜 내용을 확인할 수 없다.


5. verifyCredential 함수 실행

Verifier가 personal mobile 내의 credential 값이 유효한지 판단 후 true or false 값으로 리턴한다. 위의 경우에서는 두 값이 같기 때문에, true 값이 나왔다.


(Holder가 자신의 크리덴셜을 위,변조한 경우)


6. tamperCredential 함수 실행

백신 타입을 1 → 2로, 날짜를 21.10.3 → 22.01.12로 변경하였다.


7. verifyCredential 함수 실행

verify 해본 결과 false를 리턴하였다.

이렇게 Holder, Issuer, Verifier 세 주체를 중심으로 한 코로나19 백신접종증명 시스템을 약식으로 구현해보았다.


🔆 개발 회고

하루라는 짧은 시간 안에 약식의 시나리오를 구현하려고 했기 때문에 간소화하고, 생략하는 과정들이 많았다.

  • 암호화하지 못한 채로 정보를 주고받은 점

  • 사용자 개인 저장 공간을 구현하지 못한 점

  • 상속, Modifier 등의 다양한 솔리디티 기능을 활용해보지 못한 점

    등이 아쉬웠다. 하지만 100을 구현하기 위해 120을 알아야 한다는 것처럼, 해당 과제를 구현하기 위해 그 이상의 것들을 찾고, 공부해야 했고, 그 과정에서 배워가는 점이 많았다.

기존에 issueCredential 코드와 getCredential 코드만 있던 상태에서 함수를 더 추가해 최소한의 시나리오가 진행될 수 있도록 설계하였다.

늘 주어졌던 솔리디티 코드를 복붙하는 것을 넘어서, 내가 직접 구조를 설계하고 코드를 작성, 실행시켰던 게 가장 뿌듯한 경험이었다.

최소한의 기능만 구현해서 빠르게 과제를 마무리하는 것이 목표였는데, 하다보니 계속 욕심이 생겼다. 기존에는 크리덴셜을 발급하고 개인 모바일에 저장하는 것까지가 목표였으나 verify 과정이 추가되었고, tamperCredential 함수와 Hash 함수까지 추가되었다. 아마 시간이 더 주어졌다면 엄청난 것(?)을 구현했을지도 모른다.




🔆 Reference

https://www.slideshare.net/JaehoonJShim/ssi-introduction

https://ssimeetupkorea.github.io/vc-data-model/#core-data-model

https://ncv.kdca.go.kr/menu.es?mid=a12502000000

https://medium.com/0xcode/hashing-functions-in-solidity-using-keccak256-70779ea55bb0

profile
Prospective Entrepreneur

6개의 댓글

comment-user-thumbnail
2022년 6월 9일

이런 완벽한 글은 처음보네요 그xxx x 가셔야할듯

1개의 답글
comment-user-thumbnail
2022년 6월 9일

그저 빛,,

1개의 답글
comment-user-thumbnail
2022년 6월 9일

논문인가

1개의 답글