Issuer 웹페이지 만들기
개요
작성된 DID Document 를 VDR 에 저장해 누구든 접근할 수 있도록 합니다. 또한 이 과정을 보기 쉽도록 웹 페이지로 구성합니다.
사전 프로그램 설치
생성된 DID Document 를 저장하기 위해 VDR 을 사용합니다. 또한 VDR 통신을 위한 API 서버를 사용합니다.
API 서버와 같은 파이썬 환경에서 웹을 개발할 때 사용되는 Streamlit
라이브러리를 사용합니다.
작성된 Issuer 는 API 서버와, API 서버는 Ganache GUI 와 상호작용하기 때문에, 다음의 순서대로 실행해야 합니다.
- Ganache GUI 실행
- VDR 스마트 컨트랙트 배포
- API 서버 실행 (스마트 컨트랙트 주소 변경)
- 웹페이지 실행
공통 기능 만들기
SSI 생태계의 Issuer, Holder, Verifier 의 공통된 기능은 DID, DID Document 를 생성할 수 있다는 점입니다. 따라서, DID, DID Document 만들기 를 참고해 Account 생성과 DID, DID Document 를 생성하는 공통 기능을 별도 모듈로 작성합니다.
공통 모듈명을 kernel.py
로 지정하고, 파일은 모든 참가자들이 접근할 수 있는 위치에 생성합니다.
- 파일 위치 : ~/api/pages/peers
Account 생성 API 만들기
이전 Post 의 내용을 기반으로 다음과 같이 Account 생성 함수를 작성합니다.
from ecdsa import SigningKey, VerifyingKey, SECP256k1
def _generate_keypair() -> tuple[SigningKey, VerifyingKey]:
"""
ECDSA 키 쌍을 생성합니다.
"""
private_key = SigningKey.generate(curve=SECP256k1)
public_key = private_key.verifying_key
return private_key, public_key
def create_account() -> tuple:
"""
_generate_keypair() 함수를 호출하는 Wrapper 함수입니다.
"""
return _generate_keypair()
DID 생성 API 만들기
DID 생성 함수를 작성합니다.
import base58
def _generate_did(method_name:str, public_key:str) -> str:
"""
Public Key 로 DID 를 생성합니다.
"""
return f"did:{method_name}:{base58.b58encode(public_key.encode()).decode()}"
def create_did(method_name:str, public_key:str) -> str:
"""
_generate_did() 함수를 호출하는 Wrapper 함수입니다.
"""
return _generate_did(method_name, public_key)
DID Document 생성 API 만들기
DID Document 생성 API 를 작성합니다.
def _generate_did_document(did:str, public_key:str) -> dict:
"""
DID Document 를 생성합니다.
"""
did_document = {
"@context": "https://www.w3.org/ns/did/v1",
"id": did,
"publicKey": [
{
"id": did + "#keys-1",
"type": "EcdsaSecp256k1VerificationKey2019",
"controller": did,
"publicKeyHex": public_key
}
],
"authentication": [
{
"type": "Ed25519SignatureAuthentication2018",
"publicKey": did + "#keys-1"
}
]
}
return did_document
def create_did_doc(did:str, public_key:str) -> dict:
"""
_generate_did_document() 함수를 호출하는 Wrapper 함수입니다.
"""
return _generate_did_document(did, public_key)
Issuer 기본 페이지 만들기
작성된 공통 기능을 활용해 Issuer 페이지에서 Account 를 생성하고, DID, DID Document 를 작성하는 기능을 작성합니다.
Issuer 를 탭으로 분리하기
Issuer 는 VDR 과 분리된 별도 페이지로 생성합니다.
이를 위해 pages 디렉토리에 빈 issuer.py
파일을 생성한 후 다음과 같이 탭으로 등록합니다.
# -*- coding:utf-8 -*-
from st_pages import Page, show_pages
show_pages(
[
Page("pages/home.py", "Home" , "🏠"),
Page("pages/vdr.py", "VDR Test"),
Page("pages/issuer.py", "Issuer"),
]
)
빈 Issuer 페이지 생성
Isser 기본 페이지 만들기
작성한 kernel.py 를 사용하기 위해 Import 하고, Issuer 페이지임을 알 수 있도록 타이틀을 등록합니다.
from pages.peers import kernel
import streamlit as st
api_url = "http://localhost:8000"
st.title("Issuer (using Streamlit)")
Account 생성하기
kernel 모듈의 create_account() 를 사용해 손쉽게 Account 를 생성할 수 있습니다.
st.subheader("Account 를 생성합니다. ")
if st.button("Create Account"):
private_key, public_key = kernel.create_account()
st.write("Public Key")
st.success(f"{public_key.to_string().hex()}")
st.write("Private Key")
st.success(f"{private_key.to_string().hex()}")
Issuer Account 생성
DID 생성하기
앞서 생성한 Account 의 Public Key 를 입력해 DID 를 생성합니다.
st.subheader("DID 를 생성합니다.")
insert_public_key = st.text_input("Enter Account's Public Key : ", key="insert_public_key")
if st.button("Generate DID"):
did = kernel.create_did("dkei", insert_public_key)
st.write("DID")
st.success(f"{did}")
Issuer DID 생성
DID Document 생성하기
Account 의 Public Key 와 DID 를 입력해 DID Document 를 생성합니다.
st.subheader("DID Document 를 생성합니다.")
insert_did = st.text_input("Enter DID : ", key="insert_did")
if st.button("Generate DID Document"):
did_doc = kernel.create_did_doc(insert_did, insert_public_key)
st.write("DID Document")
st.success(f"{did_doc}")
Issuer DID Document 생성
Issuer 페이지 개선하기
API 호출하고 결과를 확인하는 기본적인 기능을 모두 작성되었습니다. 하지만, 생성한 Public Key 나 DID 를 복사해야 하는 번거로운 부분과 같은 페이지이기에 변수를 재사용하거나 Scope 등의 신경쓰이는 부분이 남았습니다. 이를 좀 더 개선하기 위해, Public Key, DID, DID Document 를 Streamlit 의 Stateful Button 을 활용해 언제든 접근할 수 있도록 수정합니다.
Stateful button 활용하기
Streamlit 에는 Session 의 State를 저장하고 언제든 활용할 수 있도록 stateful button
기능을 제공합니다.
즉, st.session_state
변수에 임의로 Attribute
를 추가하고 값을 저장하는 방식입니다.
Account 생성 코드를 살펴보면, 제일 먼저 is_exist_account
속성이 있는지 확인합니다.
없을 경우, setattr 과 같은 Attribute 추가없이 st.seesion_state.is_exist_account
로 바로 추가되고, 값을 지정할 수 있습니다.
같은 방법으로 앞으로 저정할 account
변수도 미리 초기화합니다.
st.button
에 on_click
속성에 위에 정의한 create_account
함수명을 지정하면, Create Account
버튼을 누를 때, on_click 에 정의한 create_account 함수가 호출됩니다.
create_account 함수는 Account 를 생성하고, 사전에 정의한 st.session_state.account 와 st.session_state.is_exist_account 변수에 각각 저장합니다.
#### Account
if "is_exist_account" not in st.session_state:
st.session_state.is_exist_account = False
st.session_state.account = {}
def create_account():
private_key, public_key = kernel.create_account()
st.session_state.account = {
"public_key" : public_key,
"private_key" : private_key
}
st.session_state.is_exist_account = True
st.subheader("Account 를 생성합니다. ")
st.button("Create Account", on_click=create_account)
if st.session_state.is_exist_account:
account = st.session_state.account
st.write("Public Key")
st.success(f"{account.get('public_key').to_string().hex()}")
st.write("Private Key")
st.success(f"{account.get('private_key').to_string().hex()}")
전체 코드는 다음과 같습니다.
from pages.peers import kernel
import streamlit as st
api_url = "http://localhost:8000"
st.title("Issuer (using Streamlit)")
#### Account
if "is_exist_account" not in st.session_state:
st.session_state.is_exist_account = False
st.session_state.account = {}
def create_account():
private_key, public_key = kernel.create_account()
st.session_state.account = {
"public_key" : public_key,
"private_key" : private_key
}
st.session_state.is_exist_account = True
st.subheader("Account 를 생성합니다. ")
st.button("Create Account", on_click=create_account)
if st.session_state.is_exist_account:
account = st.session_state.account
st.write("Public Key")
st.success(f"{account.get('public_key').to_string().hex()}")
st.write("Private Key")
st.success(f"{account.get('private_key').to_string().hex()}")
#### DID
if "is_exist_did" not in st.session_state:
st.session_state.is_exist_did = False
st.session_state.did = ""
def create_did():
if st.session_state.is_exist_account:
account = st.session_state.account
public_key = account.get("public_key", "").to_string().hex()
st.session_state.did = kernel.create_did("dkei", public_key)
st.session_state.is_exist_did = True
else:
st.warning("Account 를 먼저 생성하세요")
st.subheader("DID 를 생성합니다.")
st.button("Create DID", on_click=create_did)
if st.session_state.is_exist_did:
st.success(st.session_state.did)
#### DID Document
if "is_exist_did_doc" not in st.session_state:
st.session_state.is_exist_did_doc = False
st.session_state.did_doc = ""
def create_did_doc():
if st.session_state.is_exist_account and st.session_state.is_exist_did:
account = st.session_state.account
public_key = account.get("public_key", "").to_string().hex()
did = st.session_state.did
st.session_state.did_doc = kernel.create_did_doc(did, public_key)
st.session_state.is_exist_did_doc = True
else:
if st.session_state.is_exist_account == False:
st.warning("Account 를 먼저 생성하세요")
else:
st.warning("DID 를 먼저 생성하세요")
st.subheader("DID Document 를 생성합니다.")
st.button("Create DID Document", on_click=create_did_doc)
if st.session_state.is_exist_did_doc:
st.success(st.session_state.did_doc)
VDR 에 저장하기
Issuer 기본 페이지가 완성되었으므로, 생성된 DID 와 DID Document 를 VDR 에 저장합니다.
from requests.models import Response
import requests
api_url = "http://localhost:8000"
def call_register(_did:str, _document:str) -> Response:
endpoint = f"{api_url}/register"
params = {"_did": _did, "_document": _document}
return requests.post(endpoint, params=params)
def save_to_vdr():
if st.session_state.is_exist_did and st.session_state.is_exist_did_doc:
did = st.session_state.did
did_doc = st.session_state.did_doc
resp = call_register(did, did_doc)
if resp.status_code == 200:
result = resp.json()
st.success(f"Register Result : {result}")
else:
st.warning(f"[ERROR] {resp}")
st.subheader("VDR 에 저장합니다.")
st.button("Register DID, DID Document", on_click=save_to_vdr)
위와 같이 작성하면, save_to_vdr 함수에서 결과가 페이지 최상단에 표기됩니다.
save_to_vdr 함수에서 결과가 출력될 때
페이지 최하단에 출력되도록 save_to_vdr 함수 결과를 session_state 에 저장하고, 하단에서 출력되도록 수정합니다.
from requests.models import Response
import requests
api_url = "http://localhost:8000"
if "is_saved" not in st.session_state:
st.session_state.is_saved = False
st.session_state.saved_result = ""
def call_register(_did:str, _document:str) -> Response:
endpoint = f"{api_url}/register"
params = {"_did": _did, "_document": _document}
return requests.post(endpoint, params=params)
def save_to_vdr():
if st.session_state.is_exist_did and st.session_state.is_exist_did_doc:
did = st.session_state.did
did_doc = st.session_state.did_doc
resp = call_register(did, did_doc)
if resp.status_code == 200:
result = resp.json()
st.session_state.saved_result = result
st.session_state.is_saved = True
else:
st.session_state.saved_result = resp
st.subheader("VDR 에 저장합니다.")
st.button("Register DID, DID Document", on_click=save_to_vdr)
result = st.session_state.saved_result
if st.session_state.is_saved:
st.success(f"Register Result : {result}")
elif st.session_state.is_saved == False and st.session_state.saved_result != "":
st.error(f"Error : {result}")
save_to_vdr 함수 결과를 별도 처리할 때
코드 마지막 부분에서 if…else 로 처리하지 않고, elif 로 처리한 이유는 if…else 로 처리하면, 페이지 로깅시 is_saved 는 항상 False 이므로, Error 부분이 노출된 상태로 시작되므로, 매끄러워 보이지 않았습니다. 따라서, 페이지를 좀 더 깔끔하게 표현하기 위한 부분일 뿐 if…else 로 처리해도 로직상 아무런 문제가 없습니다.
댓글남기기