DID는 SSI (Self-Sovereign Idenetity)의 한 종류입니다. 중앙 집중형 신원 증명에서 탈중앙화 자기 주권 신원 증명으로 전환된 것입니다. 블록체인과 함께 등장하여 중앙 집중형 절차와 데이터 저장소를 배제하고, 신원 증명에 대한 개인 권한을 극대화했습니다.
(1) Claim 으로 정보를 확인, Digital Proof를 통해 확인
(2) Issuer와 holder의 Digital Signature 포함
(3) 발급 내역을 블록체인에 올림
(4) Schema 검증
이 과정을 통해서 4가지를 판단합니다.
그림으로 나타내면 다음과 같습니다.
(tip) Verifiable Data Registry는 블록체인이 아니라도 Issuer, Holder, Verifier가 동의하는 저장소로 대체가능합니다.
COOV를 참고하여 백신 증명을 위해 필요한 정보들을 알아봤습니다.
이를 바탕으로 Basic Vaccine System을 개발했습니다.
신원확인에 필요한 정보가 담긴 Credential 구조체는 다음과 같습니다. value에는 정보를 담은 jwt를 담아줄 것입니다.
struct Credential {
uint256 id;
address issuer;
uint8 manufacturerType;
uint8 vaccineDoseType;
string value;
uint256 createDate;
}
제조사(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";
}
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;
}
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];
}
}
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;
}
}
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];
}
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);
}
}
입력 값을 바탕으로 생성한 jwt을 value에 담아서 claimCredential을 실행하는 과정을 확인할 수 있습니다.
DIDVaccineSystem Contract Deploy link
DIDVaccineSystem claimCredential function link
전체 코드는 다음 링크에서 확인할 수 있습니다.
https://github.com/vamprodo47/did-vaccine-system