zkLogin 통합
다음은 wallet 또는 frontend application이 zkLogin-enabled transaction을 지원하기 위해 구현해야 하는 high-level flow이다:
- wallet가 ephemeral key pair를 생성한다.
- wallet가 ephemeral public key에 대응하는 nonce로 사용자가 OAuth login flow를 완료하도록 유도한다.
- JSON Web Token(JWT)을 받은 후 wallet가 zero-knowledge proof를 획득한다.
- wallet가 JWT에 기반한 고유 user salt를 획득한다. OAuth subject identifier와 salt를 사용해 zkLogin Sui address를 계산한다.
- wallet가 ephemeral private key로 transaction에 서명한다.
- wallet가 transaction을 ephemeral signature와 zero-knowledge proof와 함께 제출한다.
이제 구체적인 구현 세부사항을 살펴본다.
Install the zkLogin TypeScript SDK
프로젝트에서 zkLogin TypeScript SDK를 사용하려면 프로젝트 root에서 다음 command를 실행한다:
- npm
- Yarn
- pnpm
$ npm install @mysten/sui
$ yarn add @mysten/sui
$ pnpm add @mysten/sui
최신 experimental version을 사용하려면 다음을 실행한다:
- npm
- Yarn
- pnpm
$ npm install @mysten/sui@experimental
$ yarn add @mysten/sui@experimental
$ pnpm add @mysten/sui@experimental
Get JWT
-
ephemeral key pair를 생성한다. 전통적인 wallet에서 key pair를 생성하는 것과 같은 과정을 따른다. 자세한 내용은 Sui SDK를 참조한다.
-
ephemeral key pair의 expiration time을 설정한다. maximum epoch가 현재 epoch인지 그 이후인지 여부는 wallet가 결정한다. 이것을 사용자가 조정할 수 있는지도 wallet가 결정한다.
-
구성된 client ID, redirect URL, ephemeral public key, nonce로 OAuth URL을 조합한다. 이것이 애플리케이션이 계산된 nonce와 함께 login flow를 완료하도록 사용자를 보내는 값이다.
import { generateNonce, generateRandomness } from '@mysten/sui/zklogin';
import { SuiGrpcClient } from '@mysten/sui/grpc';
const FULLNODE_URL = 'https://fullnode.devnet.sui.io:443'; // 사용하려는 RPC URL로 바꾼다
const suiClient = new SuiGrpcClient({ baseUrl: FULLNODE_URL, network: 'devnet' });
const { epoch, epochDurationMs, epochStartTimestampMs } = await suiClient.core.getLatestSuiSystemState();
const maxEpoch = Number(epoch) + 2; // 이는 ephemeral key가 지금부터 2 epoch 동안 활성 상태임을 의미한다.
const ephemeralKeyPair = new Ed25519Keypair();
const randomness = generateRandomness();
const nonce = generateNonce(ephemeralKeyPair.getPublicKey(), maxEpoch, randomness);
auth flow URL은 $CLIENT_ID, $REDIRECT_URL, $NONCE로 구성할 수 있다.
일부 provider("Auth Flow Only"가 "Yes"인 경우)는 auth flow 직후 redirect URL에서 JWT를 즉시 찾을 수 있다.
다른 provider("Auth Flow Only"가 "No"인 경우)는 auth flow가 redirect URL에 code($AUTH_CODE)만 반환한다.
JWT를 가져오려면 "Token Exchange URL"로 추가 POST call이 필요하다.
| Provider | Auth Flow URL | Token Exchange URL | Auth Flow Only |
|---|---|---|---|
https://accounts.google.com/o/oauth2/v2/auth?client_id=$CLIENT_ID&response_type=id_token&redirect_uri=$REDIRECT_URL&scope=openid&nonce=$NONCE | N/A | Yes | |
https://www.facebook.com/v17.0/dialog/oauth?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&scope=openid&nonce=$NONCE&response_type=id_token | N/A | Yes | |
| Twitch | https://id.twitch.tv/oauth2/authorize?client_id=$CLIENT_ID&force_verify=true&lang=en&login_type=login&redirect_uri=$REDIRECT_URL&response_type=id_token&scope=openid&nonce=$NONCE | N/A | Yes |
| Kakao | https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&nonce=$NONCE | https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&code=$AUTH_CODE | No |
| Apple | https://appleid.apple.com/auth/authorize?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&scope=email&response_mode=form_post&response_type=code%20id_token&nonce=$NONCE | N/A | Yes |
| Slack | https://slack.com/openid/connect/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&nonce=$NONCE&scope=openid | https://slack.com/api/openid.connect.token?code=$AUTH_CODE&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET | Yes |
| Microsoft | https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=$CLIENT_ID&scope=openid&response_type=id_token&nonce=$NONCE&redirect_uri=$REDIRECT_URL | Yes |
Decoding JWT
redirect가 성공하면 OpenID provider가 JWT를 URL parameter로 붙인다. 다음은 Google flow를 사용하는 예시이다.
http://host/auth?id_token=tokenPartA.tokenPartB.tokenPartC&authuser=0&prompt=none
id_token param이 encoded format의 JWT이다.
jwt.io 웹사이트에 붙여넣으면 encoded token의 유효성을 검증하고 그 구조를 조사할 수 있다.
JWT를 decode하려면 jwt_decode: 같은 library를 사용하고 response를 제공된 JwtPayload type에 매핑할 수 있다:
const decodedJwt = jwt_decode(encodedJWT) as JwtPayload;
export interface JwtPayload {
iss?: string;
sub?: string; // Subject ID
aud?: string[] | string;
exp?: number;
nbf?: number;
iat?: number;
jti?: string;
}
User salt management
zkLogin은 user salt를 사용해 zkLogin Sui address를 계산한다(definition 참조).
salt는 16-byte 값이거나 2n**128n보다 작은 integer여야 한다.
애플리케이션이 user salt를 유지하는 방법에는 여러 선택지가 있다:
- Client side:
- Option 1: wallet access 중에 salt를 사용자 입력으로 받아 책임을 사용자에게 넘기고, 사용자가 이를 기억하게 한다.
- Option 2: Browser 또는 Mobile Storage: 기기나 브라우저 변경 시 사용자가 wallet access를 잃지 않도록 적절한 workflow를 보장한다. 한 가지 접근법은 새 wallet setup 중에 salt를 이메일로 보내는 것이다.
- 각 사용자에 대해 일관되게 고유 salt를 반환하는 endpoint를 노출하는 backend service.
- Option 3: 일반적인 database(예:
user또는passwordtable)에 사용자 식별자(예:sub)에서 user salt로의 mapping을 저장한다. salt는 사용자별로 고유하다. - Option 4: master seed 값을 유지하는 service를 구현하고, JWT를 검증하고 parsing한 뒤 key derivation으로 user salt를 도출한다. 예를 들어 here에 정의된
HKDF(ikm = seed, salt = iss || aud, info = sub)를 사용한다. 이 option은 master seed rotation이나 client ID(aud) 변경을 허용하지 않는다는 점에 유의한다. 그렇지 않으면 다른 user address가 도출되어 fund 손실로 이어진다.
- Option 3: 일반적인 database(예:
다음은 Mysten Labs-maintained salt server(option 4 사용)에 대한 request와 response 예시이다. Mysten Labs salt server를 사용하려면 Enoki docs를 참고하고 문의한다. whitelist된 client ID로 인증된 유효한 JWT만 허용된다.
$ curl -X POST https://salt.api.mystenlabs.com/get_salt -H 'Content-Type: application/json' -d '{"token": "$JWT_TOKEN"}'
Response: {"salt":"129390038577185583942388216820280642146"}
user salt는 Web2 credential과 Web3 credential을 연결하지 않도록 OAuth identifier(sub)를 온체인 Sui address와 분리하는 데 사용된다.
salt를 잃거나 잘못 사용하면 이 연결이 가능해질 수 있지만, fund 제어나 zkLogin asset 권한 자체가 손상되지는 않는다.
Get the user's Sui address
OAuth flow가 완료되면 redirect URL에서 JWT를 찾을 수 있다. 이 값과 user salt를 함께 사용해 zkLogin address를 다음과 같이 도출할 수 있다:
import { jwtToAddress } from '@mysten/sui/zklogin';
const zkLoginUserAddress = jwtToAddress(jwt, userSalt, false);
Get the zero-knowledge proof
다음 단계는 ZK proof를 가져오는 것이다. 이것은 ephemeral key pair가 유효하다는 것을 증명하는 ephemeral key pair에 대한 attestation(proof)이다.
먼저 ZKP의 입력으 로 사용할 extended ephemeral public key를 생성한다.
import { getExtendedEphemeralPublicKey } from '@mysten/sui/zklogin';
const extendedEphemeralPublicKey = getExtendedEphemeralPublicKey(ephemeralKeyPair.getPublicKey());
이전 ephemeral key pair가 만료되었거나 그 외의 이유로 접근할 수 없으면 새 ZK proof를 가져와야 한다.
ZK proof 생성은 client side에서 resource 집약적이고 느릴 수 있으므로, wallet는 ZK proof 생성 전용 backend service endpoint를 사용하는 것이 권장된다.
선택지는 두 가지이다:
- Mysten Labs-maintained proving service를 호출한다.
- 제공된 Docker image를 사용해 backend에서 proving service를 실행한다.
Call the Mysten Labs-maintained proving service
Mainnet용 Mysten hosted ZK Proving Service를 사용하려면 Enoki docs를 참고하고 접근 권한을 위해 문의한다.
extendedEphemeralPublicKey, jwtRandomness, salt에는 BigInt 또는 Base64 encoding을 사용할 수 있다.
다음 예시는 두 개의 sample request를 보여주며, 첫 번째는 BigInt encoding을 사용하고 두 번째는 Base64를 사용한다.
$ curl -X POST $PROVER_URL -H 'Content-Type: application/json' \
-d '{"jwt":"$JWT_TOKEN", \
"extendedEphemeralPublicKey":"84029355920633174015103288781128426107680789454168570548782290541079926444544", \
"maxEpoch":"10", \
"jwtRandomness":"100681567828351849884072155819400689117", \
"salt":"248191903847969014646285995941615069143", \
"keyClaimName":"sub" \
}'
$ curl -X POST $PROVER_URL -H 'Content-Type: application/json' \
-d '{"jwt":"$JWT_TOKEN", \
"extendedEphemeralPublicKey":"ucbuFjDvPnERRKZI2wa7sihPcnTPvuU//O5QPMGkkgA=", \
"maxEpoch":"10", \
"jwtRandomness":"S76Qi8c/SZlmmotnFMr13Q==", \
"salt":"urgFnwIxJ++Ooswtf0Nn1w==", \
"keyClaimName":"sub" \
}'
Response:
{
"proofPoints": {
"a": [
"17267520948013237176538401967633949796808964318007586959472021003187557716854",
"14650660244262428784196747165683760208919070184766586754097510948934669736103",
"1"
],
"b": [
[
"21139310988334827550539224708307701217878230950292201561482099688321320348443",
"10547097602625638823059992458926868829066244356588080322181801706465994418281"
],
[
"12744153306027049365027606189549081708414309055722206371798414155740784907883",
"17883388059920040098415197241200663975335711492591606641576557652282627716838"
],
["1", "0"]
],
"c": [
"14769767061575837119226231519343805418804298487906870764117230269550212315249",
"19108054814174425469923382354535700312637807408963428646825944966509611405530",
"1"
]
},
"issBase64Details": {
"value": "wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw",
"indexMod4": 2
},
"headerBase64": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ"
}
How to handle CORS error
Frontend app에서 발생할 수 있는 CORS error를 피하려면 이 call을 backend service로 위임하는 것이 권장된다.
response는 zkLogin SDK의 getZkLoginSignature에 대한 inputs parameter type에 매핑할 수 있다.
const proofResponse = await post('/your-internal-api/zkp/get', zkpRequestPayload);
export type PartialZkLoginSignature = Omit<
Parameters<typeof getZkLoginSignature>['0']['inputs'],
'addressSeed'
>;
const partialZkLoginSignature = proofResponse as PartialZkLoginSignature;
Run the proving service in your backend
-
zkey를 다운로드하기 전에 Git Large File Storage(large file versioning을 위한 open source Git extension)를 설치한다.
-
Groth16 proving key zkey file를 다운로드한다. 모든 Sui network에 대한 zkey가 제공된다.
-
Main zkey (Mainnet과 Testnet용)
$ wget -O - https://raw.githubusercontent.com/sui-foundation/zklogin-ceremony-contributions/main/download-main-zkey.sh | bash -
Test zkey (Devnet용)
$ wget -O - https://raw.githubusercontent.com/sui-foundation/zklogin-ceremony-contributions/main/download-test-zkey.sh | bash -
다운로드에 올바른 zkey file이 포함되어 있는지 확인하려면, 다음 command를 실행해 Blake2b hash를 검사할 수 있다:
b2sum ${file_name}.zkey.Network zkey file name Hash Mainnet, Testnet zkLogin-main.zkey060beb961802568ac9ac7f14de0fbcd55e373e8f5ec7cc32189e26fb65700aa4e36f5604f868022c765e634d14ea1cd58bd4d79cef8f3cf9693510696bcbcbceDevnet zkLogin-test.zkey686e2f5fd969897b1c034d7654799ee2c3952489814e4eaaf3d7e1bb539841047ae8ee5fdcdaca5f4ddd76abb5a8e8eb77b44b693a2ba9d4be57e94292b26ce2
-
-
다음 단계에는 mysten/zklogin repository의 Docker image 두 개(
prover,prover-fetag)가 필요하다. 이를 단순화하기 위해 이 과정을 자동화하는 docker compose file이 제공된다. YAML file과 같은 directory에서 다운로드한 zkey로docker compose를 실행한다.
services:
backend:
image: mysten/zklogin:prover-stable
volumes:
# ZKEY environment variable은 zkey file 경로로 설정되어야 한다.
- ${ZKEY}:/app/binaries/zkLogin.zkey
environment:
- ZKEY=/app/binaries/zkLogin.zkey
- WITNESS_BINARIES=/app/binaries
frontend:
image: mysten/zklogin:prover-fe-stable
command: '8080'
ports:
# PROVER_PORT environment variable은 원하는 port로 설정되어야 한다.
- '${PROVER_PORT}:8080'
environment:
- PROVER_URI=http://backend:8080/input
- NODE_ENV=production
- DEBUG=zkLogin:info,jwks
# 기본 timeout은 15초이다. 변경하려면 다음 줄의 주석을 해제한다.
# - PROVER_TIMEOUT=30
ZKEY=<path_to_zkLogin.zkey> PROVER_PORT=<PROVER_PORT> docker compose up
- service를 호출하려면 다음 두 endpoint가 지원된다:
/ping: service가 살아 있는지 테스트한다.curl http://localhost:PROVER_PORT/ping을 실행하면pong을 반환해야 한다./v1: request와 response는 Mysten Labs maintained service와 동일하다.
몇 가지 중요한 점은 다음과 같다:
-
backend service(
mysten/zklogin:prover-stable)는 compute-heavy하다. 최소 권장 사양인 16 core와 16GB RAM 이상을 사용한다. 더 약한 instance를 사용하면 "Call to rapidsnark service took longer than 15s"라는 timeout error가 발생할 수 있다.PROVER_TIMEOUTenvironment variable을 조정해 timeout 값을 다르게 설정할 수 있다. 예를 들어, timeout을 30초로 설정하려면PROVER_TIMEOUT=30을 사용한다. -
prover를 처음부터 compile하고 싶다면(성능상의 이유), rapidsnark에 대한 우리의 fork를 참조한다. prover를 server mode로 compile하고 launch해야 한다.
-
DEBUG=*를 설정하면 prover-fe service의 모든 log가 켜지며, 이 중 일부에는 PII가 포함될 수 있다. production environment에서는DEBUG=zkLogin:info,jwks사용을 고려한다.
Assemble the zkLogin signature and submit the transaction
먼저 앞서 생성한 ephemeral private key로 transaction bytes에 서명한다. 이는 traditional key pair signing과 동일하다. transaction sender도 정의되어 있어야 한다.
import { SuiGrpcClient } from '@mysten/sui/grpc';
const ephemeralKeyPair = new Ed25519Keypair();
const client = new SuiGrpcClient({ baseUrl: '<YOUR_RPC_URL>', network: 'mainnet' });
const txb = new Transaction();
txb.setSender(zkLoginUserAddress);
const { bytes, signature: userSignature } = await txb.sign({
client,
signer: ephemeralKeyPair, // 이는 ZKP request에 사용한 것과 동일한 ephemeral key pair여야 한다
});
다음으로 userSalt, sub(subject ID), aud(audience)를 결합해 address seed를 생성한다.
address seed와 partial zkLogin signature를 inputs parameter로 설정한다.
이제 ZK proof(inputs), maxEpoch, ephemeral signature(userSignature)를 결합해 zkLogin signature를 serialize할 수 있다.
import { genAddressSeed, getZkLoginSignature } from '@mysten/sui/zklogin';
const addressSeed = genAddressSeed(
BigInt(userSalt!),
'sub',
decodedJwt.sub,
decodedJwt.aud,
).toString();
const zkLoginSignature = getZkLoginSignature({
inputs: {
...partialZkLoginSignature,
addressSeed,
},
maxEpoch,
userSignature,
});
마지막으로 transaction을 실행한다.
client.executeTransaction({
transaction: bytes,
signatures: [zkLoginSignature],
});
Caching the ephemeral private key and ZK proof
앞서 설명했듯 각 ZK proof는 ephemeral key pair에 묶여 있다.
따라서 ephemeral key pair가 만료될 때까지(현재 epoch가 maxEpoch를 넘을 때까지) 같은 proof를 재사용해 원하는 수의 transaction에 서명할 수 있다.
향후 사용을 위해 ephemeral key pair와 ZKP를 함께 cache하고 싶을 수 있다.
그러나 ephemeral key pair는 전통적인 wallet의 key pair에 준하는 secret로 취급해야 한다. 이는 ephemeral private key와 ZK proof가 모두 공격자에게 노출되면, 일반적으로 앞서 설명한 같은 과정으로 사용자를 대신해 어떤 transaction이든 서명할 수 있기 때문이다.
따라서 어떤 platform에서든 안전하지 않은 storage location에 이를 영구 저장해서는 안 된다. 예를 들어 브라우저에서는 ephemeral key pair와 ZK proof를 저장할 때 local storage 대신 session storage를 사용한다. 그 이유는 session storage는 브라우저 session이 끝나면 데이터를 자동으로 지우지만, local storage의 데이터는 무기한 유지되기 때문이다.
Efficiency considerations
전통적인 signature와 비교하면 zkLogin signature는 생성에 더 긴 시간이 걸린다. 예를 들어 Mysten Labs가 유지하는 prover는 일반적으로 proof를 반환하는 데 약 3초가 걸리며, 이는 16 vCPU와 64 GB RAM을 가진 machine에서 실행된다. physical CPU 또는 graphics processing unit(GPU)을 가진 더 강력한 machine을 사용하면 proving time을 더 줄일 수 있다.
애플리케이션이 prover에 몇 건의 request를 보내야 하는지 신중하게 고려한다. 대체로 고려해야 할 올바른 metric은 signature 수가 아니라 active user session 수이다. 이는 앞서 설명했듯 같은 ZK proof를 cache해 session 전반에 걸쳐 재사용할 수 있기 때문이다. 예를 들어 하루에 백만 개의 active user session이 예상된다면, 트래픽이 고르게 분산된다고 가정할 때 초당 1~2 request(RPS)를 처리할 수 있는 prover가 필요하다.
Mysten Labs가 유지하는 prover는 트래픽 급증을 처리하도록 auto-scale로 설정되어 있다. Mysten Labs가 특정 request 수를 처리할 수 있는지 확신이 없거나, 애플리케이션이 필요로 하는 prover request 수가 갑자기 급증할 것으로 예상된다면 Discord를 통해 문의한다. 우리의 계획은 필요한 어떤 RPS라도 처리할 수 있도록 prover를 수평 확장하는 것이다.