dApp - DID를 활용한 졸업증명서 개발하기

김도영·2022년 7월 26일
0

크리덴셜(Credential)

Issuer가 제기한 하나 이상의 Claim 집합, VC(Verifiable Credential)은 암호화된 검증을 생성할 수 있는 변조 방지 Credential이다. 이 크리덴셜을 스마트 컨트랙트로 개발을 해보고자 한다.

전체 코드

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

contract CredentialBox {
    address private issuerAddress;
    uint256 private idCount;
    mapping(uint8 => string) private alumniEnum;

    struct Credential{
        uint256 id;
        address issuer;
        uint8 alumniType;
        string value;
    }

    mapping(address => Credential) private credentials;

    constructor() {
        issuerAddress = msg.sender;
        idCount = 1;
        alumniEnum[0] = "SEB";
        alumniEnum[1] = "BEB";
        alumniEnum[2] = "AIB";
    }

    function claimCredential(address _alumniAddress, uint8 _alumniType, string calldata _value) public returns(bool){
        require(issuerAddress == msg.sender, "Not Issuer");
				Credential storage credential = credentials[_alumniAddress];
        require(credential.id == 0);
        credential.id = idCount;
        credential.issuer = msg.sender;
        credential.alumniType = _alumniType;
        credential.value = _value;
        
        idCount += 1;

        return true;
    }

    function getCredential(address _alumniAddress) public view returns (Credential memory){
        return credentials[_alumniAddress];
    }

}

이 코드는 Issuer와 Credential을 포함하는 Solidity 코드이다. 이번 실습 '졸업 증명서 개발하기'는 claimCredential함수로 Credential을 발행하고, getCredential함수를 통해 Credential을 발행한 주소에서 VC를 확인하는 간단한 구조이다.

Credential 확인

struct Credential{
        uint256 id;
        address issuer;
        uint8 alumniType;
        string value;
}

위 코드는 VC를 구현하기 위한 구조체이다.
  • id: index 순서를 표기하는 idCount
  • issuer: 발급자, 하나 혹은 그 이상의 주체애 대한 클레임을 주장하고, 그 클레임으로부터 검증 가능한 크리덴셜을 생성하며 검증 가능한 크리덴셜을 보유자에게 전달하는 엔터티의 역할
  • alumniType: 졸업증명서 타입
  • value: 크리덴셜에 포함되어야 하는 암호화된 정보로 중앙화된 서버에서 제공하는 신원, 신원제공자, 엔터티, 서명 등이 JSON 형태로 저장된다.

claimCredential함수

function claimCredential(address _alumniAddress, uint8 _alumniType, string calldata _value) public returns(bool){
        require(issuerAddress == msg.sender, "Not Issuer");
				Credential storage credential = credentials[_alumniAddress];
        require(credential.id == 0);
        credential.id = idCount;
        credential.issuer = msg.sender;
        credential.alumniType = _alumniType;
        credential.value = _value;
        
        idCount += 1;

        return true;
}

claimCredential 함수를 통해 발급자(issuer)는 어떠한 주체(alumniAddress)에게 크리덴셜을 발행(claim)할 수 있게 된다.

getCredential 함수

function getCredential(address _alumniAddress) public view returns (Credential memory){
        return credentials[_alumniAddress];
}

이 함수를 통해 어떠한 주체(alumniAddress)를 통하여 발행(claim)한 크리덴셜을 확인할 수 있다.

Remix를 이용하여 스마트 컨트랙트 배포 & 사용

  1. Remix에서 credentialBox.sol이라는 파일을 만들고, 코드를 입력한다.

  1. 컨트랙트 코드를 컴파일 한다.

  1. 컨트랙트 코드를 배포한다.
    Ropsten 테스트넷에 CredentialBox코드를 배포(Deploy)한다.

  1. 배포가 완료되면, claimCredential함수를 실행한다.

  • alumniAddress: Credential을 발행받을 테스트넷 주소
  • alumniType: 졸업증명서 타입
  • value: JWT로 암호화된 토큰
// JWT로 암호화된 토큰
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWRwIjoiY29kZSBzdGF0ZXMiLCJ0eXBlIjoiYmViIiwidG9rZW4iOiJ0ZXN0IiwidmFsdWUiOiLsvZTrk5zsiqTthYzsnbTsuKAgRElEIOyImOujjOymnSDrsJzquInsnYQg7JyE7ZWcIO2BrOumrOuNtOyFnCDthYzsiqTtirgifQ.qXTgkPcK43uZ4_FBLBTFjaTsnmV9sAAekgK8BUZBt1g
  1. Credential 컨펌이 완료되면, getCredential함수를 실행한다.

  • alumniAddress: Credential을 발행받은 주소

getCredential함수를 통해 해당 주소에게 저장되어 있던 VC(검증가능한 크리덴셜)일 발행되었음을 확인할 수 있다.

이렇게 'DID를 활용한 졸업증명서 개발하기'를 통해 DID에서 이야기하는 기본적인 그래프 표기인 주체-속성-값에 대한 클레임을 개발한 것이다.

기존 실습에 다양한 기능 추가하기

다음의 코드는 기존의 개발되었던 졸업증명서 코드에 이전에 배웠던 솔리디티 문법을 활용하여 다양한 기능을 추가한 것이다.

전체 코드

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

abstract contract OwnerHelper {
    address private owner;

  	event OwnerTransferPropose(address indexed _from, address indexed _to);

  	modifier onlyOwner {
		require(msg.sender == owner);
		_;
  	}

  	constructor() {
		owner = msg.sender;
  	}

  	function transferOwnership(address _to) onlyOwner public {
        require(_to != owner);
        require(_to != address(0x0));
    	owner = _to;
    	emit OwnerTransferPropose(owner, _to);
  	}
}

abstract contract IssuerHelper is OwnerHelper {
    mapping(address => bool) public issuers;

    event AddIssuer(address indexed _issuer);
    event DelIssuer(address indexed _issuer);

    modifier onlyIssuer {
        require(isIssuer(msg.sender) == true);
        _;
    }

    constructor() {
        issuers[msg.sender] = true;
    }

    function isIssuer(address _addr) public view returns (bool) {
        return issuers[_addr];
    }

    function addIssuer(address _addr) onlyOwner public returns (bool) {
        require(issuers[_addr] == false);
        issuers[_addr] = true;
        emit AddIssuer(_addr);
        return true;
    }

    function delIssuer(address _addr) onlyOwner public returns (bool) {
        require(issuers[_addr] == true);
        issuers[_addr] = false;
        emit DelIssuer(_addr);
        return true;
    }
}

contract CredentialBox is IssuerHelper {
    uint256 private idCount;
    mapping(uint8 => string) private alumniEnum;
    mapping(uint8 => string) private statusEnum;

    struct Credential{
        uint256 id;
        address issuer;
        uint8 alumniType;
        uint8 statusType;
        string value;
        uint256 createDate;
    }

    mapping(address => Credential) private credentials;

    constructor() {
        idCount = 1;
        alumniEnum[0] = "SEB";
        alumniEnum[1] = "BEB";
        alumniEnum[2] = "AIB";
    }

    function claimCredential(address _alumniAddress, uint8 _alumniType, string calldata _value) onlyIssuer public returns(bool){
        Credential storage credential = credentials[_alumniAddress];
        require(credential.id == 0);
        credential.id = idCount;
        credential.issuer = msg.sender;
        credential.alumniType = _alumniType;
        credential.statusType = 0;
        credential.value = _value;
        credential.createDate = block.timestamp;

        idCount += 1;

        return true;
    }

    function getCredential(address _alumniAddress) public view returns (Credential memory){
        return credentials[_alumniAddress];
    }

    function addAlumniType(uint8 _type, string calldata _value) onlyIssuer public returns (bool) {
        require(bytes(alumniEnum[_type]).length == 0);
        alumniEnum[_type] = _value;
        return true;
    }

    function getAlumniType(uint8 _type) public view returns (string memory) {
        return alumniEnum[_type];
    }

    function addStatusType(uint8 _type, string calldata _value) onlyIssuer public returns (bool){
        require(bytes(statusEnum[_type]).length == 0);
        statusEnum[_type] = _value;
        return true;
    }

    function getStatusType(uint8 _type) public view returns (string memory) {
        return statusEnum[_type];
    }

    function changeStatus(address _alumni, uint8 _type) onlyIssuer public returns (bool) {
        require(credentials[_alumni].id != 0);
        require(bytes(statusEnum[_type]).length != 0);
        credentials[_alumni].statusType = _type;
        return true;
    }

}

이전에 배웠던 솔리디티 문법을 추가하였다. 단순히 발급하는 형태의 스마트 컨트랙트에서 issuer를 컨트롤하고, 발급 시간에 대한 정보를 크리덴셜에 추가하고, 상태를 변경하는 함수도 추가하였다.

Issuer 추가 & 삭제

OwnerHelper는 기존의 Ownerhelper와 동일하다. 하지만 IssuerHelper가 추가되었다. IssuerHelper에서는 Issuer를 추가하고 삭제하는 기능이 존재한다. 추가하고 삭제하는 기능은 onlyOwner로 제한되어 Owner만 컨트롤이 가능하다.

abstract contract IssuerHelper is OwnerHelper {
    mapping(address => bool) public issuers;

    event AddIssuer(address indexed _issuer);
    event DelIssuer(address indexed _issuer);

    modifier onlyIssuer {
        require(isIssuer(msg.sender) == true);
        _;
    }

    constructor() {
        issuers[msg.sender] = true;
    }

    function isIssuer(address _addr) public view returns (bool) {
        return issuers[_addr];
    }

    function addIssuer(address _addr) onlyOwner public returns (bool) {
        require(issuers[_addr] == false);
        issuers[_addr] = true;
        emit AddIssuer(_addr);
        return true;
    }

    function delIssuer(address _addr) onlyOwner public returns (bool) {
        require(issuers[_addr] == true);
        issuers[_addr] = false;
        emit DelIssuer(_addr);
        return true;
    }
}

여기서 issuer로 추가되어 있지 않다면 bool 값은 false로 나온다.

클레임 시간 추가하기(claimCredential)

cliamcredential에서 block.timestamp를 활용해 크리덴셜을 클레임한 시간을 크리덴셜에 저장할 수 있다.

function claimCredential(address _alumniAddress, uint8 _alumniType, string calldata _value) onlyIssuer public returns(bool){
        Credential storage credential = credentials[_alumniAddress];
        require(credential.id == 0);
        credential.id = idCount;
        credential.issuer = msg.sender;
        credential.alumniType = _alumniType;
        credential.statusType = 0;
        credential.value = _value;
        credential.createDate = block.timestamp;

        idCount += 1;

        return true;
}

Alumni의 타입을 추가하기

alumniType은 초기에 3가지가 제공된다.

constructor() {
        idCount = 1;
        alumniEnum[0] = "SEB";
        alumniEnum[1] = "BEB";
        alumniEnum[2] = "AIB";
}

여기서 Alumni의 타입을 추가하는 함수를 입력한다.

function addAlumniType(uint8 _type, string calldata _value) onlyIssuer public returns (bool) {
        require(bytes(alumniEnum[_type]).length == 0);
        alumniEnum[_type] = _value;
        return true;
}

Solidity 내부에는 String을 검사하는 두 가지 방법이 있다.

  • bytes로 변환하여 길이로 null인지 검사하는 방법
  • keccak256함수를 사용하여 두 스트링을 해쉬로 변환하여 비교하는 방법

위의 코드에서는 첫 번째 방법을 이용하여 내부 alumniEnum의 Type이 중복되는 타입이 존재하는지 검사했다.

Remix에서 PMB을 추가하여 getAlumniType에서 PMB를 확인 할 수 있다.

특정 사용자의 상태를 변경하는 기능(changeStatus)

changeStatus함수는 특정 사용자의 상태를 변경하는 함수이다. 해당 함수를 통해 크리덴셜 내부에 존재하는 statusType의 값을 가져와 변경할 수 있다.

function changeStatus(address _alumni, uint8 _type) onlyIssuer public returns (bool) {
        require(credentials[_alumni].id != 0);
        require(bytes(statusEnum[_type]).length != 0);
        credentials[_alumni].statusType = _type;
        return true;
}
profile
Blockchain Developer

0개의 댓글