# Implementation Guide

# Sample Source Code

아래 예제로 사용된 전체소스코드입니다.

# Initialization

Issuer나 verifier는 blockchain에 접속하기 위한 2가지 방법중 하나를 선택 할 수 있습니다.

  • Blockchain node에 직접 연결
  • 파라메타에서 제공하는 '마이아이디' 서버를 통해서 연결

어떤 방법을 선택하느냐에 따라 구현방법이 조금 달라집니다.
마이아이디 플랫폼에서 동작하는 모든 entity들은 자신의 DID를 가집니다. DID를 만들기 위해 DID Tool User Guide를 참고하십시오.
DID tool로 만들어진 DID file을 아래와 같이 로딩해서 DidKeyHolder 인스턴스를 생성합니다.

# MyID 서버를 사용

MyID서버를 사용하는 경우 DidKeyHolder 인스턴스와 MyID 서버에 연결하기 위한 IssuerService, VerifierService가 필요합니다.

// did score service
private DidService mDidService;
// MyID Issuer 서비스.
private IssuerService mIssuerService;
// MyID Verifier 서비스.
private VerifierService mVerifierService;
// MyID WAS URL. 테스트베드는 "https://iv-test.zzeung.id" 이고, 라이브는 파라메타 담당자에게 문의 하십시오.
private static final String IV_WAS_URL = "https://iv-test.zzeung.id";
// VCP message에서 공통으로 쓰이는 nonce값
private String mNonce;
// ECDHKey of Issuer
private ECDHKey mServerECDHKey;

// Initialize
public void initForMyIdServer() {
    try {
        mDidKeyHolder = Keystore.loadDidKeyHolder("P@ssw0rd", new File("/did.json"));
    } catch (Exception e) {
        e.printStackTrace();
    }

    mVerifierService = VerifierService.create(IV_WAS_URL);
    mIssuerService = IssuerService.create(IV_WAS_URL);
    try {
        mServerECDHKey = ECDHKey.generateKey(ECDHKey.CURVE_P256K);
    } catch (Exception e) {
        e.printStackTrace();
    }
    mNonce = EncodeType.HEX.encode(AlgorithmProvider.secureRandom().generateSeed(16));
}

# Blockchain에 직접 접속

Blockchain node에 직접 접속하는 경우 transaction을 생성하기 위해 지갑의 역할을 하는 KeyWallet 인스턴스가 필요합니다.
또한 DID 관련 기능을 이용하기 위해 DidService 인스턴스가 필요하고, Credential을 등록, 폐기 하기 위해 CredentialService 인스턴스가 필요합니다.

// DID와 암호화된 private key를 가지고 있는 Class
private DidKeyHolder mDidKeyHolder;
// 지갑
private KeyWallet mWallet;
// did score service
private DidService mDidService;
// vc score service
private CredentialService mVcService;
// VCP message에서 공통으로 쓰이는 nonce값
private String mNonce;
// ECDHKey of Issuer
private ECDHKey mServerECDHKey;

// Initialize
public void initForNode() {
    // nodeUrl, networkId, didScoreAddress, vcScoreAddress는 Blockchain 네트워크에 따라 변경됨.
    String nodeUrl = "https://blockchain-test.zzeung.id/api/v3";
    String networkId = "13057";
    BigInteger nid = new BigInteger(networkId);
    String didScoreAddress = "cx1d4b59ddf32c8ecc41bb17129f98093dd632e692";
    String vcScoreAddress = "cxecb23969e2538cdae37a7d0c27902c077d9a2131";
    IconService iconService = new IconService(new HttpProvider(nodeUrl));
    mDidService = new DidService(
            iconService, nid, new Address(didScoreAddress));
    mVcService = new CredentialService(
            iconService, nid, new Address(vcScoreAddress));

    try {
        mDidKeyHolder = Keystore.loadDidKeyHolder("P@ssw0rd", new File("/did.json"));
    } catch (Exception e) {
        e.printStackTrace();
    }

    Algorithm algorithm = AlgorithmProvider.create(AlgorithmProvider.Type.ES256K);
    mWallet = KeyWallet.load(
            new Bytes(algorithm.privateKeyToByte(mDidKeyHolder.getPrivateKey()))
    );
    try {
        mServerECDHKey = ECDHKey.generateKey(ECDHKey.CURVE_P256K);
    } catch (Exception e) {
        e.printStackTrace();
    }
    mNonce = EncodeType.HEX.encode(AlgorithmProvider.secureRandom().generateSeed(16));
}

# Blockchain 접근 방법 결정

설명의 편의를 위해서 아래와 같이 blockchain node에 접근하는 방법을 정의하는 변수를 하나 선언 합니다. 예제에서 NODE_CONNECT_TYPE을 기준으로 소스코드가 작성되어 있으니 참고하시기 바랍니다.

    public enum NodeConnectType {
        MYID_WAS, SCORE
    }
    private NodeConnectType NODE_CONNECT_TYPE = NodeConnectType.MYID_WAS;

    try {
        if (NODE_CONNECT_TYPE == NodeConnectType.MYID_WAS) {
            // MyID WAS로 부터 Holder DID Document 조회.
            holderDoc = mVerifierService.getDid(holderDid);
        } else {
            // Blockchain에서 Holder DID Document 조회
            holderDoc = mDidService.readDocument(holderDid);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

# DID init

Credential 발급을 위해 issuer는 DID init message을 holder에게 보냅니다. 이때 메세시 암호화를 위한 임시키 (opens new window)를 만들어서 같이 전달 합니다. DID init 메세지 규격을 참고 하십시오.

/**
 * Credential을 발급하기 위해 DID_INIT message를 생성
 */
public String createDidInitMessage() {
    ProtocolMessage protocolMessage = null;

    try {
        EphemeralPublicKey publicKey = new EphemeralPublicKey.Builder()
                .kid(mNonce)
                .epk(mServerECDHKey)
                .build();
        ClaimRequest request = new ClaimRequest.DidBuilder(ClaimRequest.Type.INIT)
                .nonce(mNonce)
                .didKeyHolder(mDidKeyHolder)
                .publicKey(publicKey)
                .version("2.0")
                .build();

        protocolMessage = new ProtocolMessage.RequestBuilder()
                .type(ProtocolType.DID_INIT)
                .claimRequest(request)
                .build();
    } catch (Exception exception) {
        exception.printStackTrace();
    }

    ProtocolMessage.SignResult didInitSignResult = protocolMessage.signEncrypt(mDidKeyHolder);
    if (!didInitSignResult.isSuccess()) {
        System.out.println("DidInit create sign fail.");
    }
    JsonObject didInitObject = didInitSignResult.getResult();
    return didInitObject.toString();
}

# Verify DID auth

Holder는 DID init을 받으면 issuer를 신뢰할수 있는지 체크 하고, holder의 DID와 메세지 복호화를 위한 holder의 임시키를 DID auth message로 만들어서 issuer에 전달 합니다. 이때 전달 되는 메세지는 암호화 되어 있습니다.
Issuer는 암호화된 DID auth message를 복호화 하고, holder의 DID auth를 검증해서 credential을 발급할지 여부를 판단 합니다.
DID auth 메세지 규격을 참고 하십시오.

// Credential을 요청하는 Holder의 DID
private String mHolderDid;
// Credential을 요청하는 Holder의 ECDH key
private ECDHKey mHolderECDHKey;

public void verifyDidAuthMessage(String DID_AUTH_JWE) {
    ProtocolMessage didAuthResPm = ProtocolMessage.valueOf(DID_AUTH_JWE);
    String keyId = didAuthResPm.getJweKid();

    if (didAuthResPm.isProtected()) {
        didAuthResPm.decryptJwe(mServerECDHKey);        // JWE -> JWT 복호화
    }

    boolean verifyResult = false;
    try {
        verifyResult = verifyDidAuth(didAuthResPm, mServerECDHKey, keyId, mNonce);
    } catch (Exception e) {
        e.printStackTrace();
    }

    if(verifyResult) {
        mHolderDid = didAuthResPm.getClaimResponseJwt().getDid();
    }
    else {
        // Fail to verify DID_AUTH_JWE
    }
}

private boolean verifyDidAuth(ProtocolMessage didAuthPm, ECDHKey issuerECDHKey, String issuerKeyId, String nonce) throws Exception {
    ClaimResponse didAuthResponse = didAuthPm.getClaimResponseJwt();
    mHolderECDHKey = didAuthPm.getJwe().getJweHeader().getEpk();

    EphemeralPublicKey holderPublicKey = new EphemeralPublicKey.Builder()
            .kid(issuerKeyId)
            .epk(mHolderECDHKey)
            .build();

    String holderDid = didAuthResponse.getDid();
    String holderKeyId = didAuthResponse.getKeyId();
    Document holderDoc = null;
    try {
        if (NODE_CONNECT_TYPE == NodeConnectType.MYID_WAS) {
            // MyID WAS로 부터 Holder DID Document 조회.
            holderDoc = mIssuerService.getDid(holderDid);
        } else {
            // Blockchain에서 Holder DID Document 조회
            holderDoc = mDidService.readDocument(holderDid);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

    if (holderDoc == null) {
        System.out.println("DidAuth holderDoc is null");
        return false;
    } else if (holderDoc != null) {
        // nonce 확인
        String holderNonce = didAuthResponse.getJwt().getPayload().getNonce();
        if (!nonce.equals(holderNonce)) {
            System.out.println("Not equals nonce value");
            return false;
        }

        PublicKeyProperty publicKeyProperty =
                holderDoc.getPublicKeyProperty(holderKeyId);
        if (publicKeyProperty == null || publicKeyProperty.isRevoked()) {
            System.out.println("DidAuth's holderDid revoked");
            return false;
        } else {
            PublicKey publicKey = publicKeyProperty.getPublicKey();
            Jwt.VerifyResult verifyResult = didAuthResponse.verify(publicKey);
            if (!verifyResult.isSuccess()) {
                System.out.println(verifyResult.getFailMessage());  // verify fail
                return false;
            }
        }
    }
    System.out.println("Verifying DidAuth succeeded.");
    return true;
}

# Create Credential

Issuer는 DID auth 확인을 마치면 credential을 발급 합니다. credential에 담기는 claim정보를 이해하기 위해서는 JSON-LD (opens new window)에 대한 이해가 필요합니다. MyID2.0 Implementation Guide 문서의 Verifiable Credential Data Model을 참고 하십시오.

Credential을 만들기 위해 VC model (opens new window)JSON-LD (opens new window)의 상세 내용을 이해할 필요는 없습니다. 아래 샘플코드의 내용중 일부를 적절히 수정 하는 것으로 Credential을 만들 수 있습니다. 아래 나오는 샘플 코드는 휴대폰 본인인증 Credential의 내용을 구현한 것입니다.

public String createCredential() {
    // credential에  담을 claim value 예시
    Map<String, Claim> claims = new LinkedHashMap<>();
    claims.put("name", new Claim("홍길순"));
    claims.put("gender", new Claim("female", "여성"));
    claims.put("phoneNumber", new Claim("01031142962", "010-3114-2962"));
    claims.put("telco", new Claim("SKT"));
    claims.put("birthDate", new Claim("2000-01-01"));
    claims.put("connectingInformation", new Claim("0000000000000000000000000000000000000000"));
    List<String> layoutList = Arrays.asList("name", "gender", "phoneNumber", "telco", "birthDate");

    DisplayLayout displayLayout = new DisplayLayout.StrBuilder().displayLayout(layoutList).build();

    JsonLdParam credentialParam = new JsonLdParam.Builder()
            .context(Arrays.asList("https://vc.zzeung.kr/credentials/v1.json",
                    "http://vc.zzeung.kr/credentials/mobile_authentication/kor/v1.json"))
            .type(Arrays.asList("CredentialParam", "MobileAuthenticationKorCredential"))
            .proofType("hash")
            .hashAlgorithm("SHA-256")
            .claim(claims)
            .displayLayout(displayLayout)
            .build();

    String nonce = EncodeType.HEX.encode(AlgorithmProvider.secureRandom().generateSeed(16));

    Credential credential = new Credential.Builder()
            .didKeyHolder(mDidKeyHolder)     // Issuer DID
            .nonce(nonce)
            .targetDid(mHolderDid)   // Holder DID
            .vcParam(credentialParam)
            .id("https://myid.id/credential/example/phone/vc/0000001")  // Credential의 ID (아무값이나 입력)
            .refreshId("https://vc.example.com/refresh_service/")   // 재발급 URL (optional)
            .refreshType("ManualRefreshService")                   // 재발급 가능함을 명시(optional)
            .version("2.0")
            .build();

    Date issued = new Date();
    // 365일짜리 유효기간을 만듬.
    long duration = credential.getDuration() * 365 * 1000L;
    Date expiration = new Date(issued.getTime() + duration);

    EphemeralPublicKey holderPublicKey = new EphemeralPublicKey.Builder()
            .kid(mNonce)
            .epk(mHolderECDHKey)
            .build();

    ProtocolMessage credentialPm = null;
    try {
        credentialPm = new ProtocolMessage.CredentialBuilder()
                .type(ProtocolType.RESPONSE_CREDENTIAL)
                .credential(credential)
                .requestPublicKey(holderPublicKey)     // received key from holder
                .issued(issued)
                .expiration(expiration)
                .build();
    } catch (Exception e) {
        e.printStackTrace();
    }

    ProtocolMessage.SignResult credentialSignResult =
            credentialPm.signEncrypt(mDidKeyHolder, mServerECDHKey);
    JsonObject credentialObj = credentialSignResult.getResult();

    if (!credentialSignResult.isSuccess()) {
        System.out.println("Signing Credential failed.");
        // mDidKeyHolder의 key store파일이 문제가 없는지 확인 필요.
        return null;
    }

    // 발급할 VC를 Blockchain에 등록
    boolean isRegisterVc = false;
    if (NODE_CONNECT_TYPE == NodeConnectType.MYID_WAS) {
        isRegisterVc = registerCredentialViaMyIDServer(credentialPm);
    } else {
        isRegisterVc = registerCredentialViaScore(credentialPm);
    }
    if(!isRegisterVc) {
        // VC를 블록체인에 등록 실패함.
        return null;
    }

    // TODO 기존에 이 사용자가 발급 받고, blockchain에 저장한 VC가 있다면 여기서 폐기 (Optional)
    String oldVcSignature = ""; // DB 에서 가지고 와야 함.
    if (NODE_CONNECT_TYPE == NodeConnectType.MYID_WAS) {
        revokeCredentialInfoViaMyIDServer(oldVcSignature);
    } else {
        revokeCredentialInfoViaScore(oldVcSignature);
    }

    // TODO 등록한 VC를 나중에 위와같은 코드로 폐기 하기위한 ID인 VC ID (VC Signature)를 DB저장 해야함.
    // TODO 아래 vcSignature를 DB에 저장하고 추 후 폐기가 필요할 때 사용해야 함.
    String vcSignature = credentialPm.getJwt().getSignature();

    return credentialObj.toString();
}

# Display Value

Credential의 Claim 실제 값은 parameter 부분에 선언되어 있습니다. 때때로 credential이 주장 하고자 하는 값과, 쯩 앱에서 화면에 노출하고자 하는 값의 형태가 다른 경우가 있습니다. 자세한 내용은 Display Value를 참고하십시오.
아래 예제에서 name은 '홍길순'이고 쯩 앱에서도 '홍길순'이라고 표시가 됩니다. gender는 'female'의 값을 가지지만 쯩 앱에서는 '여성'이라고 표기가 됩니다.

Map<String, Claim> claims = new LinkedHashMap<>();
claims.put("name", new Claim("홍길순"));
claims.put("gender", new Claim("female", "여성"));

# Display Layout

Credential의 claim 정보는 displayLayout에 의해 노출되는 순서와 노출 여부가 표시 됩니다. Claim 정보중 화면에 표시를 하지 않기를 원하다면 displayLaout에 포함하지 않으면 됩니다.

List<String> layoutList = Arrays.asList("name", "gender", "phoneNumber", "telco", "birthDate");
DisplayLayout displayLayout = new DisplayLayout.StrBuilder().displayLayout(layoutList).build();

쯩 앱의 상세화면에 claim을 표시할 때 목적이나 성격별 그룹핑해서 보여주고 싶을수 있습니다. 이때 layout group을 지정할 수 있습니다. 아래 코드는 claim들 idCardLayout과 accountLayout으로 그룹핑되어 보여지게 됩니다.

Map<String, Claim> claims = new LinkedHashMap<>();
claims.put("name", new Claim("홍길동"));
claims.put("gender", new Claim("female", "여성"));
claims.put("birthDate", new Claim("1989-10-11")); 
claims.put("connectingInformation", new Claim("f0a1928cd09ale98cfee29ppoa01")); 
claims.put("residentRegistrationNumber", new Claim("9101011234567", "910101-1******"));
claims.put("residentRegistrationNumberFirst7", new Claim("9101011", "910101-1")); 
claims.put("bank", new Claim("한국은행")); 
claims.put("accountNumber", new Claim("429201119281"));

List<String> idCardOrder = Arrays.asList("name", "gender", "birthDate", "idCardType",
        "residentRegistrationNumber", "driverLicenseNumber", "issueDate");

List<String> accountOrder = Arrays.asList("bank", "accountNumber");
Map<String, List<String>> idCardLayout = new LinkedHashMap<>();
Map<String, List<String>> accountLayout = new LinkedHashMap<>();
idCardLayout.put("idCardGroup", idCardOrder);
accountLayout.put("accountGroup", accountOrder);
DisplayLayout displayLayout = new DisplayLayout.ObjBuilder()
        .displayLayout(Arrays.asList(idCardLayout, accountLayout))
        .build();

# Display Information

Claim이 아닌 정보성 데이터를 credential에 아래와 같이 추가할 수 있습니다. 자세한 내용은 Verifiable Credential Data ModelDisplay Information을 참고하십시오.

# TextView

멀티라인 텍스트를 쯩 앱의 화면에 표시합니다.

Map<String, InfoParam> infoParam = new LinkedHashMap<>();
InfoParam textViewInfo = new InfoParam.TextViewBuilder()
                .name("VC발급 동의내역")
                .content("2021년 1월22일에 example.com에서 본인인증 동의를 하고\nVC를 발급했습니다.")
                .build();
infoParam.put("description", textViewInfo);
// credentail Param 생성 시 builder에 추가
JsonLdParam credParam = new JsonLdParam.Builder()
                .info(infoParam)       // optional
                .build();

# WebView (TBD)

Map<String, InfoParam> infoParam = new LinkedHashMap<>();
InfoParam webViewInfo = new InfoParam.WebViewBuilder()
                .name("참고할 수 있는 페이지")
                .url("https://example.com/")
                .build();
infoParam.put("description", webViewInfo);
// credentail Param 생성 시 builder에 추가
JsonLdParam credParam = new JsonLdParam.Builder()
                .info(infoParam)       // optional
                .build();

# Add Refresh Service

Credential을 갱신하는 URL을 credential 내부에 담을 수 있습니다. Credential 갱신규격에 관한 자세한 내용은 Refresh Credential을 참고하십시오.
아래와 코드와 같이 추가하십시오.

Credential credential = new Credential.Builder()
    .didKeyHolder(mDidKeyHolder)     // Issuer DID
    .refreshId("https://vc.example.com/refresh_service/")   // 재발급 URL (optional)
    .refreshType("ManualRefreshService")                   // 재발급 가능함을 명시(optional)
    .build();

# Add Revocation Service

Credential을 폐기할 수 있는 URL을 credential에 포함할 수 있습니다. Credential 폐기규격에 관한 자세한 내용은 Revocation Credential을 참고하십시오.

# ManualRevocationService (TBD)

// TODO

# SimpleRevocationService

RevocationService revocationService = new RevocationService.Builder()
        .id("https://www.example.com/credential/revoke")
        .type("SimpleRevocationService")
        .shortDescription("휴대폰 본인인증 VC를 폐기합니다.\n 이 VC는 앞으로 사용될 수 없습니다. ")
        .build();
Credential credential = new Credential.Builder()
        .revocationService(revocationService)
        .build();

# Request Presentation

Verifier는 holder가 가진 credential을 요청 합니다. Holder는 Credentialpresentation 형태로 제공을 하게 되는데, 이 presentation을 요청 하기 위한 JWT를 생성하는 방법을 설명합니다.

아래 예제는 휴대폰 본인인증 Credential을 요청하는 Request Presentation Message를 만들어 냅니다.

public String createRequestPresentationForPhoneId() {
    EphemeralPublicKey verifierPublicKey = new EphemeralPublicKey.Builder()
            .kid(mNonce)
            .epk(mServerECDHKey)
            .build();

    // 제출 요청할 항목
    List<String> requireProperty = Arrays.asList("name", "gender", "telco", "phoneNumber", "connectionInformation", "birthDate");
    // 조건 작성
    VprCondition condition = new VprCondition.SimpleBuilder()   // SimpleCondition
            .conditionId("uuid-requisite-0000-1111-2222")       // 아무값이나 입력
            .context(Arrays.asList(
                    "https://vc.zzeung.kr/credentials/mobile_authentication/kor/v1.json"))
            .credentialType("MobileAuthenticationKorCredential")
            .property(requireProperty)
            //.issuer(Arrays.asList("did:icon:01:17354.."))    // Optional. 이슈어를 지정할 경우에 입력
            .build();

    JsonLdVpr vpr = null;
    try {
        vpr = new JsonLdVpr.Builder()
                .context(Arrays.asList("https://vc.zzeung.kr/credentials/v1.json"))
                .id("https://www.ubplus.com/vpr/mobile")  // VPR ID로 아무 값이나 입력.
                .purpose("휴대폰 인증서로 본인 확인")              // VC 요청의 목적을 입력.
                .verifier(mDidKeyHolder.getDid())
                .condition(condition)
                .build();
    } catch (Exception e) {
        e.printStackTrace();
    }

    ClaimRequest reqPresentation = new ClaimRequest.PresentationBuilder()
            .didKeyHolder(mDidKeyHolder)
            .requestDate(new Date())
            .nonce(mNonce)
            .publicKey(verifierPublicKey)
            .version("2.0")
            .vpr(vpr)
            .build();

    ProtocolMessage reqPresentationPm = null;
    try {
        reqPresentationPm = new ProtocolMessage.RequestBuilder()
                .type(ProtocolType.REQUEST_PRESENTATION)
                .claimRequest(reqPresentation)
                .build();
    } catch (Exception e) {
        e.printStackTrace();
    }
    ProtocolMessage.SignResult reqPresentationSignResult =
            reqPresentationPm.signEncrypt(mDidKeyHolder);

    if (!reqPresentationSignResult.isSuccess()) {
        System.out.println("Signing RequestPresentation failed.");
        // mDidKeyHolder의 key store파일이 문제가 없는지 확인 필요.
        return null;
    }

    JsonObject reqPresentationObject = reqPresentationSignResult.getResult();
    System.out.println("VPR : " + reqPresentationObject.toString());

    return reqPresentationObject.toString();
}

# 제출 받을 VC 선택

위 예제는 휴대폰 본인인증VC를 제출 요청하는 코드입니다. 제출 받고자하는 VC의 리스트및 상세정보는 MyID Credentials 문서를 참고하십시오.
예를 들어 FinancialIdCredential(실명인증)을 필요로 한다면 requireProperty와 context, credentialType을 아래와 같이 작성할 수 있습니다.

// 제출 요청할 항목
List<String> requireProperty = Arrays.asList("name", "residentRegistrationNumber", "residentRegistrationNumberFirst7", "birthDate",
                "gender", "connectingInformation", "bank", "accountNumber");

VprCondition condition = new VprCondition.SimpleBuilder()   // SimpleCondition
    // 코드 생략
    .context(Arrays.asList("http://vc.zzeung.kr/credentials/financial_id/v1.json"))    // 실명인증!!
    .credentialType("FinancialIdCredential")                                    // 실명인증 VC Type
    .build();

# VC를 발급한 Issuer 선택

여러 issuer가 동일한 타입의 VC를 발급할 수 있고, 쯩 앱이 이런 VC들을 가지고 있을 수 있습니다. 특정 issuer가 발급한 VC를 요청 하고자 한다면 아래와 같이 issuer를 입력할 수 있습니다. 하나 이상의 issuer를 입력한 경우 쯩 앱에서는 먼저 선언 된 issuer를 우선 순위를 높여서 검색 합니다. 아래 .issuer() 를 선언하지 않는 경우 쯩 앱은 VC Type만으로 VC를 선택 합니다.

VprCondition condition = new VprCondition.SimpleBuilder()   // SimpleCondition
    // 코드 생략
    .issuer(Arrays.asList("did:icon:01:17354.."))    // Optional. 이슈어를 지정할 경우에 입력
    .build();

# Verify Presentation

Verifier는 holder로부터 받은 presentation을 다음과 같이 검증합니다.

토큰을 검증하기 위해서 필요한 iolder와 issuer의 publicKey는 blockchain에서 조회합니다.

public boolean verifyPresentationForPhoneId(String PRESENTATION_JWE) {
    ProtocolMessage presentationPm = ProtocolMessage.valueOf(PRESENTATION_JWE);
    String keyId = presentationPm.getJweKid();
    if (presentationPm.isProtected()) {
        presentationPm.decryptJwe(mServerECDHKey);
    }
    System.out.println("decryptJWT :" + presentationPm.getJwtToken());
    Presentation presentation = presentationPm.getPresentationJwt();

    String holderDid = presentation.getDid();
    String holderKeyId = presentation.getKeyId();
    Document holderDoc = null;


    try {
        if (NODE_CONNECT_TYPE == NodeConnectType.MYID_WAS) {
            // MyID WAS로 부터 Holder DID Document 조회.
            holderDoc = mVerifierService.getDid(holderDid);
        } else {
            // Blockchain에서 Holder DID Document 조회
            holderDoc = mDidService.readDocument(holderDid);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

    if (holderDoc == null) {
        System.out.println("Presentation holderDoc is null");
        // blockchain에서 Holder의 DID 확인 실패
        return false;
    }

    PublicKeyProperty publicKeyProperty =
            holderDoc.getPublicKeyProperty(holderKeyId);
    if (publicKeyProperty == null || publicKeyProperty.isRevoked()) {
        System.out.println("Presentation holderDid revoked");
        // Holder의 public key가 없거나 폐기되어서 서명 확인 불가.
        return false;
    }
    PublicKey publicKey = publicKeyProperty.getPublicKey();
    presentation.getJwt();

    try {
        Jwt.VerifyResult verifyResult = presentation.getJwt().verify(publicKey);
        if (!verifyResult.isSuccess()) {
            System.out.println(verifyResult.getFailMessage());  // verify fail
            // Holder의 서명 확인 실패.
            // 즉 이 presentation은 위조 되었거나 holder의 서명에 문제가 있음.
            return false;
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

    // presentation내의 VC 검증 시작
    for (VpCriteria criteria : presentation.getVp().getFulfilledCriteria()) {
        Credential credential = Credential.valueOf(criteria.getVc());
        // presentation을 제출한 주체가 VC의 발급받은 자와 동일한지만 확인
        if (!credential.getTargetDid().equals(holderDid)) {
            System.out.println("VC's targetDid does not match holderDid.");
            return false;
        }

        Document issuerDocument = null;
        String issuerDid = credential.getIssuerDid().getDid();
        try {
            if (NODE_CONNECT_TYPE == NodeConnectType.MYID_WAS) {
                // MyID WAS로 부터 Holder DID Document 조회.
                issuerDocument = mVerifierService.getDid(issuerDid);
            } else {
                // Blockchain에서 Holder DID Document 조회
                issuerDocument = mDidService.readDocument(issuerDid);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        if (issuerDocument == null) {
            // blockchain에서 Holder의 DID 확인 실패
            return false;
        }
        PublicKeyProperty issuerKeyProperty =
                issuerDocument.getPublicKeyProperty(credential.getKeyId());
        if (issuerKeyProperty.isRevoked()) {
            // Issuer의 key가 폐기 되었음.
            return false;
        }

        try {
            PublicKey issuerPublicKey = issuerKeyProperty.getPublicKey();
            Jwt.VerifyResult credVerifyResult = credential.getJwt().verify(issuerPublicKey);
            if (!credVerifyResult.isSuccess()) {
                // Issuer key로 VC 서명 검증 실패.
                return false;
            }
            if (!holderDid.equals(credential.getTargetDid())) {
                // VC에 있는 Holder DID와 presentation을 제출한 Holder의 DID가 다름.
                return false;
            }
        } catch (AlgorithmException e) {
            e.printStackTrace();
            return false;
        }

        // 크레덴셜의 신원정보 무결성 체크: VC 내의 claim hash 값과 전달받은 실제 값을 hash하여 비교
        if (!criteria.isVerifyParam()) {
            System.out.println("Claim parameter hash is invalid.");
            // credential 신원정보 무결성 체크 실패!
            // credential 내의 claim hash 값과 param으로 전달받은 실제 값의 hash를 비교 실패.
            // 즉 credential의 신원정보가 위변조 되었을 수 있음.
            return false;
        }

        // 휴대폰 본인인증의 claim 정보.
        Map<String, Claim> claimMap = criteria.getVcParam().getClaim();

        // 요청한 항목이 presentation에 포함 되어 있는지 확인
        if (!claimMap.containsKey("name")) {
            // name : "홍길동"
            System.out.println("Claim parameter does not have `name`");
        }
        if (!claimMap.containsKey("birthDate")) {
            // birthDate (xsd:date): "1994-05-22"
            System.out.println("Claim parameter does not have `birthDate`");
        }
    }
    return true;
}

# Verify Revocation Request

Holder가 요청하는 revocation request를 검증하고 VC의 signature를 리턴하는 코드입니다.
VC의 signature를 이용해서 해당 VC를 블록체인에서 완전히 폐기 하기 위해 Revoke Credential을 참고하십시오.

public String receiveRequestRevocation(String requestRevocationJwt) {
    ProtocolMessage protocolMessage = ProtocolMessage.valueOf(requestRevocationJwt);
    ClaimRequest claimRequest = protocolMessage.getClaimRequestJwt();

    // holder DID Document를 조회.
    String holderDid = claimRequest.getDid();
    String holderKeyId = claimRequest.getKeyId();

    Document holderDoc = null;
    try {
        if (NODE_CONNECT_TYPE == NodeConnectType.MYID_WAS) {
            // MyID WAS로 부터 Holder DID Document 조회.
            holderDoc = mVerifierService.getDid(holderDid);
        } else {
            // Blockchain에서 Holder DID Document 조회
            holderDoc = mDidService.readDocument(holderDid);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

    if (holderDoc == null) {
        System.out.println("Presentation holderDoc is null");
        // blockchain에서 Holder의 DID 확인 실패
        return null;
    }

    PublicKeyProperty publicKeyProperty =
            holderDoc.getPublicKeyProperty(holderKeyId);
    if (publicKeyProperty == null || publicKeyProperty.isRevoked()) {
        System.out.println("Presentation holderDid revoked");
        // Holder의 public key가 없거나 폐기되어서 서명 확인 불가.
        return null;
    }
    PublicKey holderPublicKey = publicKeyProperty.getPublicKey();

    try {
        Jwt.VerifyResult verifyResult = claimRequest.getJwt().verify(holderPublicKey);
        System.out.println("verifyResult:" + verifyResult);
        if (!verifyResult.isSuccess()) {
            System.out.println(verifyResult.getFailMessage());
            return null;
        }
    } catch (AlgorithmException e) {
        System.out.println("Failed to verify request_revocation jwt. msg:" + e.getMessage());
        return null;
    }

        // sig 추출
        String revocationSig = claimRequest.getSig();
        System.out.println("Signature to revoke:" + revocationSig);
        return revocationSig;
    }

# Create Revocation Response

Revocation request의 응답 메세지를 만듭니다.

public String responseRevocation(String requestRevocationJwt, boolean success) {
    ResponseResult responseResult = null;
    if (success) {
        responseResult = new ResponseResult.Builder()
                .result(true)
                .build();
    } else {
        responseResult = new ResponseResult.Builder()
                .result(false)
                .errorCode("100")
                .errorMessage("Fail to verify signature.")
                .build();
    }

    ProtocolMessage pm = ProtocolMessage.valueOf(requestRevocationJwt);
    ClaimRequest claimRequest = pm.getClaimRequestJwt();
    String holderDid = claimRequest.getDid();

    ClaimResponse claimResponse = new ClaimResponse.Builder(ClaimResponse.Type.REVOCATION)
            .didKeyHolder(mDidKeyHolder)
            .responseId(holderDid)        // issuer's did
            .version("2.0")
            .responseResult(responseResult)
            .build();

    ProtocolMessage protocolMessage = new ProtocolMessage.ResponseBuilder()
            .type(ProtocolType.RESPONSE_REVOCATION)
            .claimResponse(claimResponse)
            .build();

    ProtocolMessage.SignResult signResult = protocolMessage.signEncrypt(mDidKeyHolder);

    if (!signResult.isSuccess()) {
        System.out.println("Signing RequestRevocation failed.");
        return null;
    }

    System.out.println("JWT(RESPONE_REVOCATION) : " + signResult.getResult().toString());
    System.out.println();
    return signResult.getResult().toString();
}

# Credential Validation Registry

Issuer는 credential을 발급할 때 해당 credential의 유효성 여부를 blockchain에 등록할 수 있습니다. 그리고 적절한 시기나 사용자의 요청등에 의해 blockchain에서 폐기할 수도 있습니다. Verifier들은 이 정보를 이용해서 credential의 유효성 검사를 실시 합니다.

SDK를 이용해서 credential Info 등록/폐기에 필요한 transaction 데이터를 구성해 SCORE에 전송할 수 있고, 등록된 credential Info를 조회할 수 있습니다.

Credential을 blockchain에 등록하기 위해서는 VC SCORE에 다음의 정보들을 포함한 transaction을 전송해야 합니다.

  • Credential의 JWT token, issuer와 targetHolder의 DID
  • Issuer DID privateKey로 서명한 데이터 (credential을 서명한 Issuer 증명)
  • Credential info (발급일자, Issuer DID, Holder DID, Credential Signature)

# Register Credential

# Node 참여 Issuer

Blockchain node에 직접 참여하는 issuer인 경우 아래와 같이 생성한 credential을 등록 할 수 있습니다.

public boolean registerCredentialViaScore(ProtocolMessage credentialPm) {
    Credential credential = credentialPm.getCredentialJwt();
    Date issueDate = new Date();
    Date expiryDate = credential.getJwt().getPayload().getExp();


    CredentialInfo credentialInfo = new CredentialInfo.Builder()
            .type(foundation.icon.myid.core.PropertyName.CREDENTIAL_INFO_TYPE_REGIST)
            .issuerDid(credential.getDid())
            .sig(credentialPm.getJwt().getSignature())
            .issueDate(issueDate)
            .expiryDate(expiryDate)
            .build();

    Jwt addJwt = CredentialInfoScoreParameter.CredentialInfoParam(
            mDidKeyHolder, credentialInfo);
    String signedJwt = null;
    try {
        signedJwt = mDidKeyHolder.sign(addJwt);
        TransactionResult result = mVcService.register(mWallet, signedJwt);
        if (result.getStatus().intValue() == 0) {
            System.out.println("Registering VcScore failed.");
            // blockchain에 VC 등록 실패.
            return false;
        }
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }

    return true;
}

# MyID 서버를 사용하는 경우

public boolean registerCredentialViaMyIDServer(ProtocolMessage credentialPm) {
    Credential credential = credentialPm.getCredentialJwt();
    BaseService.ServiceResult result = null;
    try {
        result = mIssuerService.registerVC(credential, mDidKeyHolder);
    } catch (AlgorithmException e) {
        e.printStackTrace();
        return false;
    }
    if(!result.isSuccess()) {
        // register fail
        return false;
    }
    return true;
}

# Get Credential State

Credential이 블록체인에 등록되어 있는지, 폐기된 것은 아닌지 조회 합니다.

# Node에 참여하는 경우

public void getCredentialInfoViaScore() {
    String signedJwt = "eyJ..";     // Credential JWT token
    Credential credential = Credential.valueOf(signedJwt);
    String issuerDid = credential.getDid();
    String sig = Jwt.decode(signedJwt).getSignature();
    CredentialInfo credentialInfo = null;
    try {
        credentialInfo = mVcService.get(sig);
    } catch (IOException e) {
        e.printStackTrace();
    }
    // vc를 검증.
    if (credentialInfo.getIsRevoke()) {
        System.out.println("vc is revoked.");
    }
    Date expiryDate = new Date(credentialInfo.getExpiryDate() * 1000L);
    if (expiryDate.before(new Date())) {
        System.out.println("vc is expired");
    }
}

# MyID 서버를 사용하는 경우

public void getCredentialInfoViaMyIDServer() {
    String signedJwt = "eyJ..";      // Credential JWT token
    Credential credential = Credential.valueOf(signedJwt);
    String issuerDid = credential.getDid();
    String sig = Jwt.decode(signedJwt).getSignature();

    CredentialInfo credentialInfo = mIssuerService.getVC(issuerDid, sig);
    if (credentialInfo == null) {
        System.out.println("vc is null.");
    }
    // vc를 검증.
    if (credentialInfo.getIsRevoke()) {
        System.out.println("vc is revoked.");
    }
    Date expiryDate = new Date(credentialInfo.getExpiryDate() * 1000L);
    if (expiryDate.before(new Date())) {
        System.out.println("vc is expired");
    }
}

# Revoke Credential

Blockchain에 등록한 Credential을 폐기하여 사용되지 못하도록 합니다.

# Node에 참여하는 경우

public void revokeCredentialInfoViaScore(String vcSignature) {
    RevokeCredentialInfo revokedCrendentialInfo = new RevokeCredentialInfo.Builder()
            .type("REVOKE")
            .issuerDid(mDidKeyHolder.getDid())                // issuerDid
            .sig(vcSignature)                                 // 폐기할 VC의 signature
            .revokeDate(new Date())                           // 폐기하는 현재시간
            .build();

    try {
        Jwt revokeJwt = RevokeCredentialInfoScoreParameter
                .RevokeCredentialInfoParam(mDidKeyHolder, revokedCrendentialInfo);
        String signedRevokeJwt = mDidKeyHolder.sign(revokeJwt);
        TransactionResult result =
                mVcService.revoke(mWallet, signedRevokeJwt);
    } catch (TransactionException e) {
        String errorCodeString = e.getCode();
        if ("158".equals(errorCodeString)) {
            // 이미 폐기된 VC_ID
        } else if ("144".equals(errorCodeString)) {
            // 폐기할 VC_ID가 blockchain에 없음.
        } else if ("136".equals(errorCodeString)) {
            // 이슈어 private key로 jwt 서명 실패. key store 파일이 문제 없는지 확인 필요.
        } else {
            // 그 외 에러.
        }
    } catch (IOException e) {
        // Failed to revoke public key
        // 네트워크 에러
        return;
    } catch (AlgorithmException e) {
        // Failed to sign
        // key store 파일이 문제가 없는지 확인 필요.
        return;
    }
}

# MyID 서버를 사용하는 경우

public void revokeCredentialInfoViaMyIDServer(String vcSignature) {
    BaseService.ServiceResult result = null;
    try {
        result = mIssuerService.revokeVC(vcSignature, mDidKeyHolder.getDid(), mDidKeyHolder);
    } catch (AlgorithmException e) {
        // Failed to revoke VC
        e.printStackTrace();
    }
    if (!result.isSuccess()) {
        // Failed to revoke VC
    }
}