# Implementation Guide

# Sample Source Code

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

# Initialization

DID echo system을 운용할 때 blockchain을 통해 아래와 같은 기능을 수행 합니다.

  • DID 생성, 조회
  • VC 등록, 폐기, 조회

이를 위해 blockchain에 접근하기 위해서는 파라메타에서 제공하는 'MyID IV WAS' 서버를 통해 접근할 수 있습니다.

또한, 마이아이디 플랫폼에서 동작하는 모든 entity들은 자신의 DID를 가집니다. 파라메타의 파트너센터를 통해서 Issuer, Verifier를 위한 DID file을 제공받을 수 있습니다. 제공받은 DID file을 아래와 같이 로딩해서 DidKeyHolder 인스턴스를 생성합니다.

# MyID 서버 연동 방법

// Issuer의 KeyHolder. DID 및 privateKey를 가지고 있음
private DidKeyHolder issuerKeyHolder;
private String nonce;
private ECDHKey ecdhKey;
// DID 조회 및 VC 발급 시 사용하기 위한 클래스
private IssuerService issuerService;

// Holder Information
private String holderDid;
private EphemeralPublicKey holderEcdhPubKey;

// MyID 서비스를 사용하기 위한 MyID Server URL
private static final String MYID_WAS_URL = "https://iv-test.zzeung.kr";


private void init() {     
    try {
        String did = "did:icon:5000:9c87958f05ef8bdaa21767fe10b85666e25189f59cf635aa";
        KeyWallet wallet = KeyWallet.load("P@ssw0rd", new File("/did_icon_5000_9c87958f05ef8bdaa21767fe10b85666e25189f59cf635aa.json"));
        issuerKeyHolder = getDidKeyHolder(wallet, did);

        nonce = JsonLdUtil.getRandomNonce(32);
        ecdhKey = ECDHKey.generateKey(ECDHKey.CURVE_P256K);
    } catch (Exception e) {
        e.printStackTrace();
    }

    issuerService = IssuerService.create(MYID_WAS_URL);
}

private DidKeyHolder getDidKeyHolder(KeyWallet wallet, String did) throws KeyPairException {
    AlgorithmProvider.Type type = AlgorithmProvider.Type.ES256K;
    Algorithm algorithm = AlgorithmProvider.create(type);

    return new DidKeyHolder.Builder()
        .did(did)
        .keyId("key-1")
        .privateKey(algorithm.byteToPrivateKey(wallet.getPrivateKey().toByteArray()))
        .type(type)
        .build();
}

# DID init

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

/**
 * Create DID_INIT message
 * @return DID_INIT message signed by issuer's key
 */
public IvProtocolMessage createDidInit() throws Exception {
    EphemeralPublicKey serverPublicKey = generateEphemeralPublicKey(nonce, ecdhKey);
    ClaimRequest claimRequest = new ClaimRequest.DidBuilder(ClaimRequest.Type.INIT)
        .version("2.0") // 고정
        .nonce(nonce)
        .didKeyHolder(issuerKeyHolder)
        .publicKey(serverPublicKey)
        .build();

    ProtocolMessage protocolMessage = new ProtocolMessage.RequestBuilder()
        .type(ProtocolType.DID_INIT)
        .claimRequest(claimRequest)
        .build();

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

    if (!signResult.isSuccess()){
        throw new Exception();  // 서명 실패
    }
    
    return new IvProtocolMessage(signResult.getResult());
}

private EphemeralPublicKey generateEphemeralPublicKey(String nonce, ECDHKey ecdhKey) {
    return new EphemeralPublicKey.Builder()
        .kid(nonce)
        .epk(ecdhKey)
        .build();
}

# Verify DID auth

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

/**
 * Verify DID_AUTH message received from the holder
 * @param didAuthMessage DID_AUTH message signed and encrypted by the holder
 * @return Result of verification
 */
public boolean verifyDidAuth(IvProtocolMessage ivProtocolMessage) throws Exception {
    // parse did_auth message
    ProtocolMessage authMessage = ProtocolMessage.valueOf(ivProtocolMessage.toString());
    
    if (authMessage.isProtected()) {
        authMessage.decryptJwe(ecdhKey);    // decrypt did_auth message (JWE -> JWT)
    }

    // verify did_auth message (JWT)
    boolean authVerifyResult = verifyProtocolMessage(authMessage, ProtocolType.DID_AUTH.getTypeName());
    if (!authVerifyResult) {
        // did_auth message 서명 검증 실패
        return false;
    }

    String nonce = authMessage.getClaimResponseJwt().getNonce();
    if(!this.nonce.equals(nonce)) {
        return false;  // nonce unmatched
    }

    return true;
}

private boolean verifyProtocolMessage(ProtocolMessage protocolMessage, String type) throws Exception {
    if (type == null) throw new IllegalArgumentException("The type for VCP message is null!");

    if (!protocolMessage.getType().equalsIgnoreCase(type)) {
        throw new Exception();  // VCP message's type is mismatched
    }

    Jwt jwt = protocolMessage.getJwt();

    boolean verifyResult = verifyJwt(jwt);
    if (!verifyResult) {
        return false;
    }
    
    // VCP message 발급 시간 확인
    verifyResult = jwt.verifyIat(3 * 60 * 1000L).isSuccess();
        return verifyResult;
}

private boolean verifyJwt(Jwt jwt) throws Exception {
    String did = jwt.getHeader().getKid().split("#")[0];    // jwt's getDid()
    String keyId = jwt.getHeader().getKid().split("#")[1];  // jwt's getKeyId()
        
    // document 조회
    Document doc = issuerService.getDid(did);
    if (doc == null) {
        return false;   // Failed to get DID Document from Blockchain
    }

    PublicKeyProperty publicKeyProperty = doc.getPublicKeyProperty(keyId);
    if (publicKeyProperty == null || publicKeyProperty.isRevoked()) {
        // DID Document is invalid
        return false;
    } else {
        PublicKey publicKey = publicKeyProperty.getPublicKey();
        Jwt.VerifyResult verifyResult = jwt.verify(publicKey);

        return verifyResult.isSuccess();
    }
}

# 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의 내용을 구현한 것입니다.

/**
 * Create and register credential
 * @return CREDENTIAL message signed and encrypted
 */
public IvProtocolMessage issueCredential() throws Exception {
    Map<String, Object> claims = new HashMap<String, Object>();
    claims.put("name", "홍길동");
    claims.put("birthDate", "19981010");
    claims.put("gender", "1");
    claims.put("telco", "LGT");
    claims.put("phoneNumber", "01012345678");
    claims.put("connectingInformation", "ddd/wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww8==");
    claims.put("citizenship", "0");

    MobileAuthenticationKorCredential mobileAuthenticationKorCredential = new MobileAuthenticationKorCredential();
    mobileAuthenticationKorCredential.setClaims(claims);
    ProtocolMessage credProtocolMessage = mobileAuthenticationKorCredential.createCredential(
            nonce,
            holderDid,
            holderEcdhPubKey,
            issuerKeyHolder,
            365 * 24 * 60 * 60 * 1000L);   // VC 유효 기간 1년
    ProtocolMessage.SignResult credSignResult = credProtocolMessage.signEncrypt(issuerKeyHolder, this.ecdhKey);
    if(!Boolean.TRUE.equals(credSignResult.isSuccess())) {
        // VC 서명 실패
        throw new Exception();
    }

    Credential credential = Credential.valueOf(credProtocolMessage.getJwt());
    BaseService.ServiceResult registerResult = issuerService.registerVC(credential, issuerKeyHolder);
    if(!Boolean.TRUE.equals(registerResult.isSuccess())) {
        // Blockchain에 VC 등록 실패
        throw new Exception();
    }

    return new IvProtocolMessage(credSignResult.getResult());
}

public class MobileAuthenticationKorCredential { 
    public static final String VC_TYPE = "MobileAuthenticationKorCredential";
    public static final String ZZEUNG_CONTEXT = "https://vc.zzeung.kr/credentials/v1.json";
    public static final String VC_CONTEXT = "https://vc.zzeung.kr/credentials/mobile_authentication/kor/v1.json";

    private String name;
    private String birthDate;
    private String gender;
    private String telco;
    private String phoneNumber;
    private String connectingInformation;
    private String citizenship;
    private List<String> contexts = Arrays.asList(VC_CONTEXT, ZZEUNG_CONTEXT);

    public void setClaims(Map<String, Object> claims) {
        this.name = (String) claims.get("name");
        this.birthDate = (String) claims.get("birthDate");
        this.gender = (String) claims.get("gender");
        this.telco = (String) claims.get("telco");
        this.phoneNumber = (String) claims.get("phoneNumber");
        this.connectingInformation = (String) claims.get("connectingInformation");
        this.citizenship = (String) claims.get("citizenship");
    }

    /**
    * credential 안에 들어갈 credential claim 을 생성한다.
    */
    public Map<String, Claim> getClaim() {
        Map<String, Claim> vcClaims = new LinkedHashMap<>();
        vcClaims.put("name", getNameClaim());
        vcClaims.put("birthDate", getBirthDateClaim());
        vcClaims.put("gender", getGenderClaim());
        vcClaims.put("phoneNumber", getPhoneNumberClaim());
        vcClaims.put("telco", getTelcoClaim());
        vcClaims.put("connectingInformation", getConnectingInformationClaim());
        vcClaims.put("citizenship", getCitizenshipClaim());
        return vcClaims;
    }

    public DisplayLayout getDisplayLayout() {
        List<String> layoutList = new ArrayList<>(Arrays.asList("name", "gender", "phoneNumber",
              "telco", "birthDate", "citizenship"));
        return new DisplayLayout.StrBuilder()
            .displayLayout(layoutList)
            .build();
    }

    /**
    * credential 을 생성하기 위한 vc param 생성
    * @return vc param object
    */
    public JsonLdParam createJsonLdParam() {
        return new JsonLdParam.Builder()
                .context(Arrays.asList(VC_CONTEXT, ZZEUNG_CONTEXT))
                .type(VC_TYPE)
                .proofType("hash")
                .hashAlgorithm("SHA-256")
                .claim(getClaim())
                .displayLayout(getDisplayLayout())
                .build();
    }   

    public ProtocolMessage createCredential(String nonce, String targetDid, EphemeralPublicKey holderPublicKey,
                                         DidKeyHolder issuerKeyHolder, long validPeriod) {
        if (holderPublicKey == null) {
            throw new IllegalArgumentException("holderPublicKey is null");
        }

        Credential credential = new Credential.Builder()
                .didKeyHolder(issuerKeyHolder)
                .nonce(nonce)
                .targetDid(targetDid)
                .vcParam(createJsonLdParam())
                .id("https://phoneiss.zzeung.kr/v1/vc/" + targetDid)
                .version("2.0")
                .build();

        Date issued = new Date();
        Date expiration = new Date(issued.getTime() + validPeriod);
        return new ProtocolMessage.CredentialBuilder()
                .type(ProtocolType.RESPONSE_CREDENTIAL)
                .credential(credential)
                .requestPublicKey(holderPublicKey)     // received key from holder
                .issued(issued)
                .expiration(expiration)
                .build();
    }

    public Claim getBirthDateClaim() {
        birthDate = birthDate.substring(0, 4) + "-" + birthDate.substring(4, 6) + "-" + birthDate.substring(6);
        String displayValue = birthDate.replaceAll("-", ".");
        return new Claim(birthDate, displayValue);
    }

    public Claim getNameClaim() {
        return new Claim(name);
    }

    public Claim getGenderClaim() { 
        String displayValue = gender.equals("male") ? "남성" : "여성";
        return new Claim(gender, displayValue);
    }

    public Claim getTelcoClaim() {
        return new Claim(telco);
    }

    public Claim getPhoneNumberClaim() {
        int middleLen = phoneNumber.length() == 11 ? 4 : 3;
        String displayValue = phoneNumber.substring(0, 3) + 
                "-" + phoneNumber.substring(3, 3 + middleLen) + 
                "-" + phoneNumber.substring(3 + middleLen);

        return new Claim(phoneNumber, displayValue);
    }

    public Claim getConnectingInformationClaim() {
        return new Claim(connectingInformation);
    }

    public Claim getCitizenshipClaim() {    // 0: 내국인, 1: 외국인
        boolean isCitizen = citizenship.equals("0");
        String displayValue = isCitizen ? "내국인" : "외국인";
        return new Claim(isCitizen, displayValue);
    }
}

# Display Value

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

public Claim getNameClaim() {
    return new Claim(name);
}

public Claim getGenderClaim() {
    String displayValue = gender.equals("male") ? "남성" : "여성";
    return new Claim(gender, displayValue);
}

# Display Layout

Credential의 claim 정보는 displayLayout에 의해 노출되는 순서와 노출 여부가 표시 됩니다.
노출 순서는 addClaim() 을 호출 하는 순서 입니다.
화면에 표시 하지 않을 claim은 displayLayout의 배열에 포함하지 않으면 됩니다.

public DisplayLayout getDisplayLayout() {     
    List<String> layoutList = new ArrayList<>(Arrays.asList("name", "gender", "phoneNumber",
        "telco", "birthDate", "citizenship"));
    
    return new DisplayLayout.StrBuilder()
        .displayLayout(layoutList)
        .build();
}

# Request Presentation

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

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

/**
 * Create VPR message
 * @return VPR message
 */
public IvProtocolMessage createVerifiablePresentationRequest() throws Exception {
    String nonce = JsonLdUtil.getRandomNonce(32);
    String conditionId = UUID.randomUUID().toString();
    VprCondition condition = new VprCondition.SimpleBuilder()   // 제출 받을 VC 선택
        .conditionId(conditionId)
        .context(Collections.singletonList("https://vc.zzeung.kr/credentials/mobile_authentication/kor/v1.json"))
        .credentialType(MobileAuthenticationKorCredential.VC_TYPE)
        .property(Arrays.asList("name", "birthDate", "phoneNumber"))
        .build();
    JsonLdVpr vpr = new JsonLdVpr.Builder()
        .context(Collections.singletonList("https://vc.zzeung.kr/credentials/v1.json"))
        .id("https://phonevrf.zzeung.kr/v1/verifier/vpr/" + nonce)
        .presentationUrl("https://phonevrf.zzeung.kr/v1/verifier/vp")
        .purpose("Confirmation of identity information")
        .verifier(didKeyHolder.getDid())
        .condition(condition)
        .build();
    ClaimRequest requestPresentation = new ClaimRequest.PresentationBuilder()
        .didKeyHolder(didKeyHolder)
        .requestDate(new Date())
        .nonce(nonce)
        .publicKey(generateEphemeralPublicKey(nonce, ecdhKey))
        .version("2.0")
        .vpr(vpr)
        .build();
    ProtocolMessage pm = new ProtocolMessage.RequestBuilder()
        .type(ProtocolType.REQUEST_PRESENTATION)
        .claimRequest(requestPresentation)
        .build();
    ProtocolMessage.SignResult signResult = pm.signEncrypt(didKeyHolder);
    if (!signResult.isSuccess()) {
        throw new Exception();  // vpr 생성 실패
    }

    return new IvProtocolMessage(signResult.getResult());
}

# Verify Presentation

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

  • Holder가 만들어서 제출한 presentation이 맞고 위변조가 없는지 서명 검증
  • presentation에 포함된 credential이 issuer가 발급했고, 위변조가 없는지 서명 검증
/**
 * Verify presentation and extract claims
 * @param VP_MESSAGE Verifiable presentation message signed and encrypted by the holder
 * @return Result of verification
 */
public boolean verifyPresentation(IvProtocolMessage ivProtocolMessage) throws Exception {
    ProtocolMessage vpMessage = ProtocolMessage.valueOf(ivProtocolMessage.toString());
    // presentation 서명 확인
    if (!verifyVp(vpMessage)) {
        return false;
    }

    Presentation presentation = vpMessage.getPresentationJwt();
    JsonLdVp jsonLdVp = presentation.getVp();
    VpCriteria vpCriteria = jsonLdVp.getFulfilledCriteria().get(0);

    // check credential's type
    List<String> vcTypes = vpCriteria.getVcParam().getTypes();
    if(!vcTypes.contains(MobileAuthenticationKorCredential.VC_TYPE)) {
        // 제출 받고자 하는 VC와 제출 받은 VC의 타입이 일치하지 않는 경우
        return false;
    }

    return true;
}

private boolean verifyVp(ProtocolMessage vpMessage) throws Exception {
    if (vpMessage.isProtected()) {
        vpMessage.decryptJwe(ecdhKey);
    }

    // presentation 서명 확인
    boolean vpVerifyResult = verifyProtocolMessage(vpMessage, ProtocolType.RESPONSE_PRESENTATION.getTypeName());
    if (!vpVerifyResult) {
        return false;
    }

    Presentation presentation = vpMessage.getPresentationJwt();
    VpCriteria criteria = presentation.getVp().getFulfilledCriteria().get(0);
    Credential credential = Credential.valueOf(criteria.getVc());

    // presentation sender 와 credential target did 확인 (동일한 holder인지 확인)
    if (!presentation.getDid().equals(credential.getTargetDid())) {
        return false;
    }

    // credential 서명 확인
    if (!verifyJwt(credential.getJwt())) {
        return false;
    }

    // check vc is revoked or expired
    String issuerDID = credential.getDid();
    String vcSig = credential.getJwt().getSignature();
    CredentialInfo credentialInfo = issuerService.getVC(issuerDID, vcSig);
    if (credentialInfo == null) {
        return false;   // 블록체인에 등록되지 않은 VC
    }
    if(credentialInfo.getIsRevoke() != null && credentialInfo.getIsRevoke()) {
        return false;   // 폐기된 VC
    }
    Date expiryDate = new Date(credentialInfo.getExpiryDate()*1000L);
    if(expiryDate.before(new Date())) {
        return false;   // 만료된 VC
    }

    // param 과 credentialSubject verify
    if (!criteria.isVerifyParam()) {
        return false;
    }

    return true;
}

# Process Revocation Request

Holder가 요청하는 revocation request를 검증하고 해당 credential을 폐기 합니다.

/**
 * Process revocation request.
 * @param REVOCATION_REQUEST_MESSAGE
 * @return
 */
public boolean revokeVc(IvProtocolMessage ivProtocolMessage) throws Exception {
    // parse request_revocation message
    ProtocolMessage revokeMessage = ProtocolMessage.valueOf(ivProtocolMessage.toString());

    // verify request_revocation message (JWT)
    boolean revokeVerifyResult = verifyProtocolMessage(revokeMessage, ProtocolType.REQUEST_REVOCATION.getTypeName());
    if (!revokeVerifyResult) {
        return false;
    }

    Payload payload = revokeMessage.getJwt().getPayload();
    String sig = payload.getSig();
    if (sig == null) {
        return false;
    }

    BaseService.ServiceResult revokeResult = issuerService.revokeVC(sig, issuerKeyHolder.getDid(), issuerKeyHolder);
    if (!Boolean.TRUE.equals(revokeResult.isSuccess())) {
        return false;
    }

    return true;
}