DID 개발
1. DID란
DID는 SSI (Self-Sovereign Idenetity)의 한 종류입니다. 중앙 집중형 신원 증명에서 탈중앙화 자기 주권 신원 증명으로 전환된 것입니다. 블록체인과 함께 등장하여 중앙 집중형 절차와 데이터 저장소를 배제하고, 신원 증명에 대한 개인 권한을 극대화했습니다.
2. DID 검증 과정
(1) Claim 으로 정보를 확인, Digital Proof를 통해 확인
(2) Issuer와 holder의 Digital Signature 포함
(3) 발급 내역을 블록체인에 올림
(4) Schema 검증
이 과정을 통해서 4가지를 판단합니다.
- Issuer DID
- holder DID
- 블록체인 상의 발급 내역
- 형식에 맞는 Schema
그림으로 나타내면 다음과 같습니다.
(tip) Verifiable Data Registry는 블록체인이 아니라도 Issuer, Holder, Verifier가 동의하는 저장소로 대체가능합니다.
3. DID 기반 백신 시스템 개발
(1) 백신 증명을 위한 정보들
COOV를 참고하여 백신 증명을 위해 필요한 정보들을 알아봤습니다.
- 접종차수(VaccineDose)
- 백신제조사(Manufacturer)
- 접종일자
이를 바탕으로 Basic Vaccine System을 개발했습니다.
(2) Basic Vaccine System
- Credential struct
신원확인에 필요한 정보가 담긴 Credential 구조체는 다음과 같습니다. value에는 정보를 담은 jwt를 담아줄 것입니다.
struct Credential {
uint256 id;
address issuer;
uint8 manufacturerType;
uint8 vaccineDoseType;
string value;
uint256 createDate;
}
- constructor
제조사(manufacturer)와 접종차수(vaccineDose)에 대한 정보를 확인할 수 있습니다.
constructor() {
issuerAddress = msg.sender;
idCount = 1;
manufacturerEnum[0] = "Astrazeneca";
manufacturerEnum[1] = "Janssen";
manufacturerEnum[2] = "Pfizer";
manufacturerEnum[3] = "Moderna";
manufacturerEnum[4] = "Novavax";
vaccineDoseEnum[0] = "1st";
vaccineDoseEnum[1] = "2nd";
vaccineDoseEnum[2] = "3rd";
vaccineDoseEnum[3] = "4th";
vaccineDoseEnum[4] = "5th";
vaccineDoseEnum[5] = "6th";
vaccineDoseEnum[6] = "7th";
vaccineDoseEnum[7] = "8th";
vaccineDoseEnum[8] = "9th";
vaccineDoseEnum[9] = "10th";
vaccineDoseEnum[10] = "11th";
vaccineDoseEnum[11] = "12th";
vaccineDoseEnum[12] = "13th";
vaccineDoseEnum[13] = "14th";
vaccineDoseEnum[14] = "15th";
vaccineDoseEnum[15] = "16th";
}
- claimCredential function
Credential을 발급하는 함수를 작성합니다. require문을 통해서 issuer만 사용가능하도록 하고, credential에 필요한 정보들을 담아줍니다.
function claimCredential(address _userAddress, uint8 _manufacturerType, uint8 _vaccineDoseType, string calldata _value) public returns(bool){
require(issuerAddress == msg.sender, "You are not a issuer");
Credential storage credential = credentials[_userAddress];
require(credential.id == 0);
credential.id = idCount;
credential.issuer = msg.sender;
credential.manufacturerType = _manufacturerType;
credential.vaccineDoseType = _vaccineDoseType;
credential.value = _value;
credential.createDate = block.timestamp;
idCount += 1;
return true;
}
- getCredential function
userAddress를 통해 발급한 credential을 받아오는 함수를 작성합니다.
function getCredential(address _userAddress) public view returns(Credential memory) {
return credentials[_userAddress];
}
- 전체 코드
위에 설명된 것들이 종합된 전체 코드는 다음과 같습니다.
//SPDX-License-Identifier : GPL-3.0
pragma solidity >= 0.7.0 <0.9.0;
contract DIDVaccineSystem {
address private issuerAddress;
uint256 private idCount;
mapping(uint8=>string) private manufacturerEnum;
mapping(uint8=>string) private vaccineDoseEnum;
struct Credential {
uint256 id;
address issuer;
uint8 manufacturerType;
uint8 vaccineDoseType;
string value;
uint256 createDate;
}
mapping(address=> Credential) private credentials;
constructor() {
issuerAddress = msg.sender;
idCount = 1;
manufacturerEnum[0] = "Astrazeneca";
manufacturerEnum[1] = "Janssen";
manufacturerEnum[2] = "Pfizer";
manufacturerEnum[3] = "Moderna";
manufacturerEnum[4] = "Novavax";
vaccineDoseEnum[0] = "1st";
vaccineDoseEnum[1] = "2nd";
vaccineDoseEnum[2] = "3rd";
vaccineDoseEnum[3] = "4th";
vaccineDoseEnum[4] = "5th";
vaccineDoseEnum[5] = "6th";
vaccineDoseEnum[6] = "7th";
vaccineDoseEnum[7] = "8th";
vaccineDoseEnum[8] = "9th";
vaccineDoseEnum[9] = "10th";
vaccineDoseEnum[10] = "11th";
vaccineDoseEnum[11] = "12th";
vaccineDoseEnum[12] = "13th";
vaccineDoseEnum[13] = "14th";
vaccineDoseEnum[14] = "15th";
vaccineDoseEnum[15] = "16th";
}
function claimCredential(address _userAddress, uint8 _manufacturerType, uint8 _vaccineDoseType, string calldata _value) public returns(bool){
require(issuerAddress == msg.sender, "You are not a issuer");
Credential storage credential = credentials[_userAddress];
require(credential.id == 0);
credential.id = idCount;
credential.issuer = msg.sender;
credential.manufacturerType = _manufacturerType;
credential.vaccineDoseType = _vaccineDoseType;
credential.value = _value;
credential.createDate = block.timestamp;
idCount += 1;
return true;
}
function getCredential(address _userAddress) public view returns(Credential memory) {
return credentials[_userAddress];
}
}
(3) Advanced Vaccine System
- Issuer를 추가하고 삭제하는 기능을 만들었습니다.
- Manufacturer를 추가하는 기능을 만들었습니다.
- block.timestamp를 보기 쉬운 DateTime으로 변환해주는 코드를 작성했습니다. 시간 변환 기능이 필요하다면 DateTime.sol을 import한 후, 상속하여 사용할 수 있습니다.
IssuerHelper
- transferOwnership을 통해 권한을 넘겨줄 수 있습니다.
- OwnerHelper를 상속하여 IssuerHelper를 구현했습니다. addIssuer를 통해 issuer를 추가하고, deleteIssuer를 통해 issuer를 삭제할 수 있습니다.
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 DeleteIssuer(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 deleteIssuer(address _addr) onlyOwner public returns (bool) {
require(issuers[_addr] == true);
issuers[_addr] = false;
emit DeleteIssuer(_addr);
return true;
}
}
manufacturer 추가와 제거
- addManufacturerType을 통해 manufacturer를 추가할 수 있습니다.
- getManufacturerType을 통해 manufacturer를 확인할 수 있습니다.
function addManufaturerType(uint8 _type, string calldata _manufacturer) onlyIssuer public returns(bool) {
require(bytes(manufacturerEnum[_type]).length == 0);
manufacturerEnum[_type] = _manufacturer;
return true;
}
function getManufaturerType(uint8 _type) public view returns(string memory) {
return manufacturerEnum[_type];
}
DateTime.sol
DIDVaccineSystem.sol에 import하여 사용하지 않았지만, 사용자가 앱을 사용하거나 정보를 얻을 때 credential 생성 시간을 좀 더 직관적으로 알 수 있도록 하는 DateTime.sol을 작성했습니다. 상황에 따라 DIDVaccineSystem.sol에 import하고, parseTimestamp function을 사용합니다. 결과적으로 blocktime을 Year, Month, Day로 변환하여 credential에 정보를 담을 수 있습니다. 전체 코드는 다음과 같습니다.
//SPDX-License-Identifier : GPL-3.0
pragma solidity >= 0.7.0 < 0.9.0;
contract DateTime {
/*
* Date and Time utilities for ethereum contracts
*
*/
struct _DateTime {
uint16 year;
uint8 month;
uint8 day;
}
uint constant DAY_IN_SECONDS = 86400;
uint constant YEAR_IN_SECONDS = 31536000;
uint constant LEAP_YEAR_IN_SECONDS = 31622400;
uint16 constant ORIGIN_YEAR = 1970;
function isLeapYear(uint16 year) private pure returns (bool) {
if (year % 4 != 0) {
return false;
}
if (year % 100 != 0) {
return true;
}
if (year % 400 != 0) {
return false;
}
return true;
}
function leapYearsBefore(uint year) private pure returns (uint) {
year -= 1;
return year / 4 - year / 100 + year / 400;
}
function getDaysInMonth(uint8 month, uint16 year) private pure returns (uint8) {
if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) {
return 31;
}
else if (month == 4 || month == 6 || month == 9 || month == 11) {
return 30;
}
else if (isLeapYear(year)) {
return 29;
}
else {
return 28;
}
}
function parseTimestamp(uint timestamp) internal pure returns (_DateTime memory dt) {
uint secondsAccountedFor = 0;
uint buf;
uint8 i;
// Year
dt.year = getYear(timestamp);
buf = leapYearsBefore(dt.year) - leapYearsBefore(ORIGIN_YEAR);
secondsAccountedFor += LEAP_YEAR_IN_SECONDS * buf;
secondsAccountedFor += YEAR_IN_SECONDS * (dt.year - ORIGIN_YEAR - buf);
// Month
uint secondsInMonth;
for (i = 1; i <= 12; i++) {
secondsInMonth = DAY_IN_SECONDS * getDaysInMonth(i, dt.year);
if (secondsInMonth + secondsAccountedFor > timestamp) {
dt.month = i;
break;
}
secondsAccountedFor += secondsInMonth;
}
// Day
for (i = 1; i <= getDaysInMonth(dt.month, dt.year); i++) {
if (DAY_IN_SECONDS + secondsAccountedFor > timestamp) {
dt.day = i;
break;
}
secondsAccountedFor += DAY_IN_SECONDS;
}
}
function getYear(uint timestamp) private pure returns (uint16) {
uint secondsAccountedFor = 0;
uint16 year;
uint numLeapYears;
// Year
year = uint16(ORIGIN_YEAR + timestamp / YEAR_IN_SECONDS);
numLeapYears = leapYearsBefore(year) - leapYearsBefore(ORIGIN_YEAR);
secondsAccountedFor += LEAP_YEAR_IN_SECONDS * numLeapYears;
secondsAccountedFor += YEAR_IN_SECONDS * (year - ORIGIN_YEAR - numLeapYears);
while (secondsAccountedFor > timestamp) {
if (isLeapYear(uint16(year - 1))) {
secondsAccountedFor -= LEAP_YEAR_IN_SECONDS;
}
else {
secondsAccountedFor -= YEAR_IN_SECONDS;
}
year -= 1;
}
return year;
}
function getMonth(uint timestamp) private pure returns (uint8) {
return parseTimestamp(timestamp).month;
}
function getDay(uint timestamp) private pure returns (uint8) {
return parseTimestamp(timestamp).day;
}
function getHour(uint timestamp) private pure returns (uint8) {
return uint8((timestamp / 60 / 60) % 24);
}
}
(4) 배포와 동작 과정 확인하기
배포
배포 etherscan
배포 완료
동작 과정 gif
입력 값을 바탕으로 생성한 jwt을 value에 담아서 claimCredential을 실행하는 과정을 확인할 수 있습니다.
Verify and Publish
Contract 배포 링크
DIDVaccineSystem Contract Deploy link
claimCredential 실행 링크
DIDVaccineSystem claimCredential function link
(5) 개발 회고
- DID 시스템을 이해하고 간단한 코드를 작성하면서 신원 증명을 위한 시스템을 알아봤습니다. 실생활에서 사용한 앱을 참고하여 개발하니까, 좀 더 구체적인 과정을 이해하는데 도움이 됐습니다.
- block.timestamp를 사용해서 credential 생성 시간을 기록했지만, 사용자 입장을 고려하면 DateTime으로 나타내는 것이 적합하다고 생각합니다. DateTime.sol을 작성하여 import하면 datetime으로 나타낼 수 있도록 했지만 좀 더 효율적인 방법을 알아내지 못한 것이 아쉽습니다.
- COOV에서는 제출 과정에서 성명, 생년월일, 국적, 여권번호, 사진을 선택적으로 추가 제공하는 것이 가능합니다. 또한 인증완료 등의 상태가 있습니다. 이를 코드로 구현하는 것을 시도해볼 수 있습니다.
전체 코드는 다음 링크에서 확인할 수 있습니다.
did-vaccine-system
Author And Source
이 문제에 관하여(DID 개발), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@vamprodo47/DID-개발저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)