Skip to main content

zkLogin

zkLogin是Sui的一种基元,它使你能够使用OAuth凭证从Sui地址发送交易,而无需公开将两者关联起来。

zkLogin设计时考虑了以下目标:

  • 简化入门流程: zkLogin使你能够使用熟悉的OAuth登录流程在Sui上进行交易,消除了处理加密密钥或记住助记词的摩擦。
  • 自我托管: zkLogin交易需要用户通过标准的OAuth登录流程进行批准,OAuth提供商不能代表用户进行交易。
  • 安全性: zkLogin是一种两因素身份验证方案:发送交易需要来自最近OAuth登录的凭据和不由OAuth提供商管理的盐。攻击者即使攻击了OAuth帐户,也不能从用户对应的Sui地址进行交易,除非它们单独攻击了盐。
  • 隐私: 零知识证明阻止第三方将Sui地址与其相应的OAuth标识符关联起来。
  • 可选的已验证身份: 用户可以选择验证用于派生特定Sui地址的OAuth标识符。这为可验证的链上身份层奠定了基础。
  • 可访问性: 由于Sui的密码学灵活性,zkLogin是几种原生Sui签名方案之一。它与其他Sui基元(如赞助交易和多重签名)集成。
  • 严谨性: zkLogin的代码已由两家专门从事零知识的公司进行了独立的审计。用于创建通用参考字符串的公共zkLogin仪式吸引了100多名参与者的贡献。

你是希望将zkLogin集成到你的应用程序或钱包中的开发者吗?深入了解我们的集成指南

如果你想了解zkLogin的工作原理,包括如何生成零知识证明以及Sui如何验证zkLogin交易,请参阅此部分

如果你对zkLogin的安全模型和隐私考虑感兴趣,请访问此页面

还有更多问题吗?请查看FAQ部分

OpenID提供商

以下表列出了可以支持zkLogin或目前正在审核中以确定它们是否可以支持zkLogin的OpenID提供商。

ProviderCan support?DevnetTestnetMainnet
FacebookYesYesYesYes
GoogleYesYesYesYes
TwitchYesYesYesYes
SlackYesYesNoNo
KakaoYesYesNoNo
AppleYesYesNoNo
RedBullUnder reviewNoNoNo
MicrosoftUnder reviewNoNoNo
AWS (Tenant)Under reviewNoNoNo
AmazonUnder reviewNoNoNo
WeChatUnder reviewNoNoNo
Auth0Under reviewNoNoNo
OktaUnder reviewNoNoNo

集成指南

以下是支持zkLogin启用交易的钱包或前端应用程序必须实现的高级流程:

  1. 钱包创建一个临时KeyPair。
  2. 钱包提示用户完成OAuth登录流程,其中包含与临时公钥对应的随机数。
  3. 在收到JWT令牌后,钱包获取零知识证明。
  4. 钱包基于JWT令牌获取唯一的用户盐。OAuth主题标识符和盐可用于计算zkLogin Sui地址。
  5. 钱包使用临时私钥签署交易。
  6. 钱包提交交易,包括临时签名和零知识证明。

让我们深入了解具体的实现细节。

安装zkLogin TypeScript SDK

要在项目中使用zkLogin TypeScript SDK,请在项目根目录中运行以下命令:

npm install @mysten/zklogin

# If you want to use the latest experimental version:
npm install @mysten/zklogin@experimental

配置与OpenID提供商的开发者账户

Sui目前支持Google、Facebook和Twitch。将来将启用更多兼容OpenID的提供商。

例如,以下TypeScript代码可用于构建用于测试的登录URL。

const REDIRECT_URI = '<YOUR_SITE_URL>';

const params = new URLSearchParams({
// See below for how to configure client ID and redirect URL
client_id: $CLIENT_ID,
redirect_uri: $REDIRECT_URL,
response_type: 'id_token',
scope: 'openid',
// See below for details about generation of the nonce
nonce: nonce,
});

const loginURL = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;

你必须在每个提供商中配置客户端ID($CLIENT_ID)和重定向URL($REDIRECT_URL):

Google

  1. 注册Google Cloud账户并访问仪表板
  2. 选择"APIs & Services"然后选择"Credentials",在那里可以找到客户端ID。设置重定向URL。这应该是钱包或应用程序前端。

1

注册Google开发者账户

2

转到Credentials

3

配置重定向URL

Facebook

  1. 注册Facebook开发者账户并访问仪表板

  2. 选择"Build your app",然后选择"Products",然后选择"Facebook Login",在那里可以找到客户端ID。设置重定向URL。这应该是钱包或应用程序前端。

1

注册Facebook开发者账户

2

转到Settings

Twitch

  1. 注册Twitch开发者账户。访问仪表板

  2. 转到"Register Your Application",然后选择"Application",在那里可以找到客户端ID。设置重定向URL。这应该是钱包或应用程序前端。

1

注册Twitch开发者账户

2

转到Console

Kakao

  1. 注册Kakao开发者账户。访问仪表板并添加一个应用。

1

向Kakao添加应用

  1. 转到"App Keys",在那里可以找到不同平台对应的客户端ID。
  • Native app key:用于通过Android或iOS SDK调用API。
  • JavaScript key:用于通过JavaScript SDK调用API。
  • REST API key:用于通过REST API调用API。

2

找到Client ID

  1. 切换打开"Kakao Login Activation"和"OpenID Connect Activation"。在"Product Settings"下的"Kakao Login"中设置重定向URL。这应该是钱包或应用程序前端。

1

设置重定向URL

Slack

  1. 注册Slack开发者账户。访问仪表板并转到"Create New App",然后选择"From scratch"。

    1

    在Slack中创建应用

  2. 在"App Credentials"下找到Client ID和Client Secret。

    1

    找到Client ID和Client Secret

  3. 在"Features"下的"OAuth & Permissions"中设置重定向URL。这应该是钱包或应用程序前端。

    1

    设置重定向URL

获取JWT令牌

  1. 生成一个临时KeyPair。按照在传统钱包中生成KeyPair的相同过程进行。详细信息请参见 Sui SDK

  2. 为临时KeyPair设置过期时间。钱包决定最大时期是当前时期还是以后的时期。钱包还确定是否用户可以调整此设置。

  3. 使用配置的客户端ID、重定向URL、临时公钥和随机数组装OAuth URL:这是应用程序发送给用户以完成登录流程的内容,其中包括计算的随机数

import { generateNonce, generateRandomness } from '@mysten/zklogin';

const FULLNODE_URL = 'https://fullnode.devnet.sui.io'; // replace with the RPC URL you want to use
const suiClient = new SuiClient({ url: FULLNODE_URL });
const { epoch, epochDurationMs, epochStartTimestampMs } = await suiClient.getLatestSuiSystemState();

const maxEpoch = Number(epoch) + 2; // this means the ephemeral key will be active for 2 epochs from now.
const ephemeralKeyPair = new Ed25519Keypair();
const randomness = generateRandomness();
const nonce = generateNonce(ephemeralKeyPair.getPublicKey(), maxEpoch, randomness);

T认证流URL可以使用 $CLIENT_ID$REDIRECT_URL$NONCE 构建。

对于某些提供商(在“仅认证流”中选择“是”),JWT令牌可以立即在认证流之后的重定向URL中找到。

对于其他提供商(在“仅认证流”中选择“否”),认证流只返回重定向URL中的代码($AUTH_CODE)。要检索JWT令牌,需要使用“Token Exchange URL”进行额外的POST调用。

ProviderAuth Flow URLToken Exchange URLAuth Flow Only
Googlehttps://accounts.google.com/o/oauth2/v2/auth?client_id=$CLIENT_ID&response_type=id_token&redirect_uri=$REDIRECT_URL&scope=openid&nonce=$NONCEN/AYes
Facebookhttps://www.facebook.com/v17.0/dialog/oauth?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&scope=openid&nonce=$NONCE&response_type=id_tokenN/AYes
Twitchhttps://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=$NONCEN/AYes
Kakaohttps://kauth.kakao.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&nonce=$NONCEhttps://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&code=$AUTH_CODENo
Applehttps://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=$NONCEN/AYes
Slackhttps://slack.com/openid/connect/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&nonce=$NONCE&scope=openidhttps://slack.com/api/openid.connect.token?code=$AUTH_CODE&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRETNo

解码JWT

在成功重定向后,身份提供商将JWT令牌作为URL参数附加(以Google流程为例)

http://host/auth?id_token=tokenPartA.tokenPartB.tokenPartC&authuser=0&prompt=none

id_token 参数是以编码格式的JWT令牌。你可以通过将其粘贴到 jwt.io 网站中来验证编码令牌的正确性并调查其结构。

要解码JWT,你可以使用类似 jwt_decode 的库,并将响应映射到提供的类型 JwtPayload


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;
}

用户盐管理

在计算zkLogin Sui地址时使用用户盐(请参见 定义)。盐必须是一个16字节的值,或者小于2n**128n的整数。应用程序在维护用户盐时有几个选项:

  1. 客户端端:
    • 选项1:在访问钱包时请求用户输入盐,将责任转移到用户身上,用户需要记住盐。
    • 选项2:浏览器或移动存储:确保适当的工作流程,以防止用户在更改设备或浏览器时失去钱包访问。一种方法是在设置新钱包时通过电子邮件发送盐。
  2. 公开一个端点的后端服务,该端点为每个用户一致返回唯一的盐。
    • 选项3:在传统数据库中(例如 userpassword 表)存储从用户标识符(例如 sub)到用户盐的映射。每个用户的盐是唯一的。
    • 选项4:实现一个服务,保留主种子值,并通过验证和解析JWT令牌派生用户盐,使用密钥派生,例如使用 HKDF(ikm = seed, salt = iss || aud, info = sub)这里定义。请注意,此选项不允许在主种子上进行旋转或更改客户端ID(即aud),否则将导致派生不同用户地址,可能会导致资金丢失。

以下是Mysten Labs维护的盐服务器的示例请求和响应(使用选项4)。如果你希望使用Mysten运行的盐服务器,请联系我们以将你注册的客户端ID列入白名单。只有使用已列入白名单的客户端ID进行身份验证的有效JWT令牌才会被接受。

curl -X POST https://salt.api.mystenlabs.com/get_salt -H 'Content-Type: application/json' -d '{"token": "$JWT_TOKEN"}'

Response: {"salt":"129390038577185583942388216820280642146"}

用户盐用于断开OAuth标识符(sub)与链上Sui地址之间的关联,以避免将Web2凭据与Web3凭据关联起来。尽管失去或滥用盐可能会启用此关联,但不会危及资金控制或zkLogin资产权限。请参阅更多讨论 这里

获取用户的Sui地址

一旦OAuth流程完成,JWT令牌可以在重定向URL中找到。结合用户盐,可以按照以下步骤派生zkLogin地址:

import { jwtToAddress } from '@mysten/zklogin';

const zkLoginUserAddress = jwtToAddress(jwt, userSalt);

获取零知识证明

下一步是获取ZK证明。这是对临时密钥对的一种证明,证明临时密钥对是有效的。

首先,生成扩展的临时公钥,以将其用作ZKP的输入。

import { getExtendedEphemeralPublicKey } from '@mysten/zklogin';

const extendedEphemeralPublicKey = getExtendedEphemeralPublicKey(ephemeralKeyPair.getPublicKey());

如果之前的临时密钥对已过期或无法访问,你需要获取一个新的ZK证明。

由于在客户端生成ZK证明可能会消耗大量资源并且潜在地较慢,建议钱包使用专用于ZK证明生成的后端服务端点。

有两个选项:

  1. 调用Mysten Labs维护的证明服务
  2. 在你的后端使用提供的Docker映像运行证明服务。

调用Mysten Labs维护的证明服务

如果你希望在Mainnet上使用Mysten运行的ZK Proving Service,请联系我们,以便将你的注册客户端ID列入白名单。仅接受使用列入白名单的客户端ID进行身份验证的有效JWT令牌。

要使用prover-dev端点,你无需列入白名单客户端ID。请注意,使用prover-dev端点生成的证明只能提交用于Devnet zkLogin交易,将其提交到Testnet或Mainnet将失败。

网络Prover URL
Mainnet, Testnethttps://prover.mystenlabs.com/v1
Devnethttps://prover-dev.mystenlabs.com/v1

你可以使用BigInt或Base64编码来处理 extendedEphemeralPublicKeyjwtRandomnesssalt。以下示例展示了两个样本请求,第一个使用BigInt编码,第二个使用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" \
}'

回应:

{
"proofPoints":{
"a":["17267520948013237176538401967633949796808964318007586959472021003187557716854",
"14650660244262428784196747165683760208919070184766586754097510948934669736103",
"1"],
"b":[["21139310988334827550539224708307701217878230950292201561482099688321320348443",
"10547097602625638823059992458926868829066244356588080322181801706465994418281"],
["12744153306027049365027606189549081708414309055722206371798414155740784907883",
"17883388059920040098415197241200663975335711492591606641576557652282627716838"],
["1","0"]],

"c":["14769767061575837119226231519343805418804298487906870764117230269550212315249",
"19108054814174425469923382354535700312637807408963428646825944966509611405530","1"]
},
"issBase64Details":{"value":"wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw", "indexMod4": 2 },
"headerBase64":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ"
}

如何处理CORS错误

为了避免在前端应用中可能出现的CORS错误,建议将此调用委托给后端服务。

响应可以映射到 zkLogin SDK 的 getZkLoginSignature 方法的输入参数类型。

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;

在你的后端运行证明服务

  1. 从 Docker Hub 仓库 下载标记为 proverprover-fe 的两个镜像。

  2. 下载 Groth16 证明密钥 zkey 文件 ,稍后将用作运行 prover 的参数。有适用于 Mainnet 和 Testnet 的 zkeys,以及用于 Devnet 的测试 zkey。有关主证明密钥生成的详细信息,请参阅 the Ceremony section。在下载 zkey 前,请安装 git lfs,这是必需的。

    • Main zkey (for Mainnet and Testnet)

      wget -O - https://raw.githubusercontent.com/sui-foundation/zklogin-ceremony-contributions/main/download-main-zkey.sh | bash
    • Test zkey (for Devnet)

      wget -O - https://raw.githubusercontent.com/sui-foundation/zklogin-ceremony-contributions/main/download-test-zkey.sh | bash
    • 验证下载是否包含正确的 zkey 文件,可以运行以下命令检查 Blake2b 哈希:b2sum ${file_name}.zkey

      Networkzkey file nameHash
      Mainnet, TestnetzkLogin-main.zkey060beb961802568ac9ac7f14de0fbcd55e373e8f5ec7cc32189e26fb65700aa4e36f5604f868022c765e634d14ea1cd58bd4d79cef8f3cf9693510696bcbcbce
      DevnetzkLogin-test.zkey686e2f5fd969897b1c034d7654799ee2c3952489814e4eaaf3d7e1bb539841047ae8ee5fdcdaca5f4ddd76abb5a8e8eb77b44b693a2ba9d4be57e94292b26ce2
  3. PORT1 上使用下载的 zkey 运行 prover。在 Linux-based 机器 (amd64) 上运行此命令。建议在生产环境中使用至少 16 vcpu,最好是 AMD cpu,并且至少需要 16GB RAM。而在测试环境中,8 vcpu 和 8GB RAM 可以工作(但速度较慢)。

    docker run \
    -e ZKEY=/app/binaries/zkLogin.zkey \
    -e WITNESS_BINARIES=/app/binaries \
    -v <path_to_zkLogin.zkey>:/app/binaries/zkLogin.zkey \
    -p PORT1:8080 \
    <prover-image>
  4. PORT2 上运行 prover-fe

    docker run \
    -e PROVER_URI='http://localhost:PORT1/input' \
    -e NODE_ENV=production \
    -e DEBUG=zkLogin:info,jwks \
    -p PORT2:8080 \
    <prover-fe-image> 8080
  5. prover-fe 服务适当地暴露,同时将 prover 服务保持为内部服务。

  6. 调用证明服务的两个支持的端点:

    • /ping:用于测试服务是否正常运行。运行 curl http://localhost:PORT2/ping 应返回 pong
    • /v1:请求和响应与 Mysten Labs 维护的服务相同。

一些建议注意事项:

  • 如果出于性能原因,你想从头开始编译 prover,请查看我们对 rapidsnark 的分支。你需要编译并以服务器模式启动 prover。

  • 设置 DEBUG=* 会在 prover-fe 服务中打开所有日志,其中一些可能包含个人身份信息(PII)。在生产环境中,考虑使用 DEBUG=zkLogin:info,jwks

  • 如果你看到错误消息 "Call to rapidsnark service took longer than 15s",请考虑通过将环境变量 PROVER_TIMEOUT 设置为较高的值来增加超时时间。例如,PROVER_TIMEOUT=30

组装 zkLogin 签名并提交交易

首先,使用先前生成的密钥对,使用临时私钥对交易字节进行签名。这与 传统的 KeyPair 签名 相同。确保交易中定义了 sender

 const ephemeralKeyPair = new Ed25519Keypair();

const client = new SuiClient({ url: "<YOUR_RPC_URL>" });

const txb = new TransactionBlock();

txb.setSender(zkLoginUserAddress);

const { bytes, signature: userSignature } = await txb.sign({
client,
signer: ephemeralKeyPair, // This must be the same ephemeral key pair used in the ZKP request
});

接下来,通过组合 userSaltsub(主题 ID)和 aud(受众)生成地址种子。

将地址种子和部分 zkLogin 签名设置为 inputs 参数。

现在,你可以通过组合 ZK 证明(inputs)、maxEpoch 和短暂签名(userSignature)来序列化 zkLogin 签名。

import { genAddressSeed, getZkLoginSignature } from "@mysten/zklogin";

const addressSeed : string = genAddressSeed(BigInt(userSalt!), "sub", decodedJwt.sub, decodedJwt.aud).toString();

const zkLoginSignature : SerializedSignature = getZkLoginSignature({
inputs: {
...partialZkLoginSignature,
addressSeed
},
maxEpoch,
userSignature,
});

最后,执行交易。

client.executeTransactionBlock({
transactionBlock: bytes,
signature: zkLoginSignature,
});

缓存短暂私钥和 ZK 证明

如先前记录的,每个 ZK 证明与一个短暂密钥对相关联。因此,你可以重用该证明来签署任意数量的交易,直到短暂密钥对过期(直到当前纪元超过 maxEpoch 为止)。

你可能希望缓存短暂密钥对以及 ZKP 供将来使用。

但是,短暂密钥对需要像传统钱包中的密钥对一样对待为机密信息。这是因为如果短暂私钥和 ZK 证明都被泄露给攻击者,那么它们通常可以代表用户签署任何交易(使用先前描述的相同过程)。

因此,你不应该在任何平台的不安全存储位置上持久存储它们。例如,在浏览器上,使用会话存储而不是本地存储来存储短暂密钥对和 ZK 证明。

zkLogin 工作原理

在简要概述中,zkLogin 协议依赖于以下内容:

  1. JWT 令牌是来自 OAuth 提供程序的已签名有效负载,包括一个名为 nonce 的用户定义字段。zkLogin 利用 OpenID Connect OAuth 流程,将 nonce 定义为公钥和到期时期。
  2. 钱包存储一个短暂 KeyPair,其中短暂公钥在 nonce 中定义。短暂私钥为短暂会话签署交易,消除了用户记忆的需要。 基于 JWT 令牌生成 Groth16 零知识证明,隐藏了隐私敏感字段。
  3. 提交具有短暂签名和 ZK 证明的交易上链。Sui 当局在验证短暂签名和证明后执行交易。
  4. 与基于公钥派生 Sui 地址不同,zkLogin 地址是从 sub(唯一标识每个提供程序的用户)、iss(标识提供程序)、aud(标识应用程序)和 user_salt(将 OAuth 标识符与链上地址解除链接的值)派生的。

完整的 zkLogin 流程

1

(步骤 0) 我们在协议的 zkSNARK 实例化中使用 Groth16,需要生成与电路相关联的结构化公共参考字符串 (CRS)。进行仪式生成 CRS,用于在 ZK 证明服务中生成证明密钥和在 Sui 当局生成验证密钥。有关详细信息,请参阅 仪式部分

(步骤 1-3) 用户首先通过登录到 OpenID 提供程序 (OP) 来获取包含定义的 nonce 的 JWT 令牌。特别是,用户生成一个 短暂 KeyPair (eph_sk, eph_pk),并将 eph_pk 与到期时间 (max_epoch) 和随机性 (jwt_randomness) 一起嵌入 nonce 中(参见 definition)。用户完成 OAuth 登录流程后,在应用程序的重定向 URL 中可以找到 JWT 令牌。

(步骤 4-5) 然后,应用程序前端将 JWT 令牌发送到 salt 服务。salt 服务在验证 JWT 令牌后,基于 iss, aud, sub 返回唯一的 user_salt

(步骤 6-7) 用户将 JWT 令牌、用户 salt、短暂公钥、jwt 随机性、密钥声明名称(即 sub)发送给 ZK 证明服务。证明服务生成一个零知识证明,将这些作为私有输入,并执行以下操作:(a) 检查 nonce 是否正确派生 如定义 (b) 检查密钥声明值是否与 JWT 中的相应字段匹配,(c) 验证来自 OP 的 JWT 的 RSA 签名,以及 (d) 地址是否与密钥声明值和用户 salt 一致。

(步骤 8): 应用程序基于 iss, aud, sub, aud 计算用户地址。只要应用程序具有有效的 JWT 令牌,可以独立执行此步骤。

(步骤 9-10) 使用短暂私钥签署交易以生成短暂签名。最后,用户将交易与短暂签名、ZK 证明和其他输入一起提交给 Sui。

(步骤 10 之后) 在链上提交后,Sui 当局使用存储中的提供程序 JWK 验证 ZK 证明,并验证短暂签名。

实体

  1. 应用程序前端:描述支持 zkLogin 的钱包或前端应用程序。该前端负责存储短暂私钥,引导用户完成 OAuth 登录流程,创建和签署 zkLogin 交易。

  2. Salt 备份服务:负责返回每个唯一用户的盐。有关维护盐的其他策略,请参阅 集成指南

  3. ZK 证明服务:负责基于 JWT 令牌、JWT 随机性、用户 salt 和最大纪元生成 ZK 证明的后端服务。此证明与 zkLogin 交易的短暂签名一起提交到链上。

地址定义

地址是根据以下输入计算的:

  1. 地址标志:zk_login_flag = 0x05 用于 zkLogin 地址。这充当 密码敏捷性 中定义的签名方案的域分隔符。

  2. kc_name_F = hashBytesToField(kc_name, maxKCNameLen):密钥声明的名称,例如 sub。使用 hashBytesToField 将字节序列映射到 BN254 中的字段元素(下文定义)。

  3. kc_value_F = hashBytesToField(kc_value, maxKCValueLen):密钥声明的值,使用 hashBytesToField 映射。

  4. aud_F = hashBytesToField(aud, maxAudValueLen):依赖方 (RP) 标识符。请参阅 定义

  5. iss:OpenID 提供程序 (OP) 标识符。请参阅 定义

  6. user_salt:用于解除 OAuth 标识符与链上地址的关联的值。

最后,我们派生 zk_login_address = Blake2b_256(zk_login_flag, iss_L, iss, addr_seed),其中 addr_seed = Poseidon_BN254(kc_name_F, kc_value_F, aud_F, Poseidon_BN254(user_salt)

术语和符号

下面是在 规范 中定义的所有相关 OpenID 术语,以及它们在 zkLogin 中的使用,以及协议详细信息的定义。

OpenID 提供程序 (OP)

能够对最终用户进行身份验证并向依赖方提供有关身份验证事件和最终用户的声明的 OAuth 2.0 授权服务器。在 JWT 令牌负载中通过 iss 字段标识。当前 zkLogin 支持的 OP 包括 Google、Facebook 和 Twitch,将来将启用更多兼容的提供程序。

依赖方 (RP) 或客户端

需要最终用户身份验证和来自 OpenID 提供程序的声明的 OAuth 2.0 客户端应用程序。由开发人员创建应用程序时由 OP 分配。在 JWT 令牌负载中通过 aud 字段标识。这指的是任何支持 zkLogin 的钱包或应用程序。

主题标识符 (sub)

在颁发者内部唯一且永不重新分配的最终用户标识符,旨在由 RP 消耗。Sui 将其用作导出用户地址的密钥声明。

JWK (JSON Web Key)

表示 OP 的一组公钥的 JSON 数据结构。可以查询公共端点(例如 https://www.googleapis.com/oauth2/v3/certs)以检索与提供程序的 kid 对应的有效公钥。与 JWT 令牌头部中的 kid 匹配后,可以根据负载及其相应的 JWK 验证 JWT 令牌。在 Sui 中,所有当局独立调用 JWK 端点,并在协议升级期间更新所有支持的提供程序的最新 JWK 视图。JWK 的正确性由验证者的法定数量(2f+1)保证。

JWT 令牌 (JSON Web Token)

JWT 令牌可以在用户完成 OAuth 登录流程后在 RP 的重定向 URL 中找到(即 https://redirect.com?id_token=$JWT_TOKEN)。JWT 令牌包含一个 headerpayload 和一个 signature。签名是可以根据 jwt_message = header + . + payload 及其由 kid 标识的 JWK 验证的 RSA 签名。payload 包含许多声明的 JSON,是名称-值对。请参阅下文,了解与 zkLogin 协议相关的特定声明。

Header

NameExample ValueUsage
algRS256zkLogin only supports RS256 (RSA + SHA-256).
kidc3afe7a9bda46bae6ef97e46c95cda48912e5979Identifies the JWK that should be used to verify the JWT.
typJWTzkLogin only supports JWT.

Payload

NameExample ValueUsage
isshttps://accounts.google.comA unique identifier assigned to the OAuth provider.
aud575519200000-msop9ep45u2uo98hapqmngv8d8000000.apps.googleusercontent.comA unique identifier assigned to the relying party by the OAuth provider.
noncehTPpgF7XAKbW37rEUS6pEVZqmoIA value set by the relying party. The zkLogin enabled wallet is required to set this to the hash of ephemeral public key, an expiry time and a randomness.
sub110463452167303000000A unique identifier assigned to the user.

对于 zkLogin 交易,我们不使用 iatexp 声明(即时间戳)。这是因为我们为用户提供了一种通过 nonce 指定到期时间的不同方式。

Key Claim

我们将用于推导用户地址的声明称为 "key claim",例如 sub 或 email。当然,我们只希望使用一次固定并且永远不再更改的声明。对于 zkLogin,我们目前支持 sub 作为 key claim,因为 OpenID 规范要求提供程序不更改此标识符。在将来,这可以扩展为使用 email、username 等。

符号表示

  1. (eph_sk, eph_pk):短暂密钥对指的是用于生成短暂签名的私钥和公钥对。签名机制与传统交易签名相同,但它是短暂的,因为它仅在短会话中存储,并且可以在新的 OAuth 会话上刷新。短暂公钥用于计算 nonce。
  2. nonce:一个嵌入在 JWT 令牌载荷中的应用程序定义字段,计算为短暂公钥、JWT 随机性和最大纪元(Sui 定义的到期纪元)的哈希。具体而言,一个与 zkLogin 兼容的 nonce 需要作为 nonce = ToBase64URL(Poseidon_BN254([ext_eph_pk_bigint / 2^128, ext_eph_pk_bigint % 2^128, max_epoch, jwt_randomness]).to_bytes()[len - 20..]) 传递,其中 ext_eph_pk_bigintext_eph_pk 的 BigInt 表示。
  3. ext_eph_pk:短暂公钥的字节表示形式(flag || eph_pk)。其大小取决于签名方案的选择(由标志表示,在 Signatures 中定义)。
  4. user_salt:引入的值,用于取消关联 OAuth 标识符与链上地址。
  5. max_epoch:JWT 令牌过期的纪元。这是 Sui 中使用的 u64。
  6. kc_name:键声明名称,例如 sub
  7. kc_value:键声明值,例如 110463452167303000000
  8. hashBytesToField(str, maxLen):使用 Poseidon 哈希将 ASCII 字符串哈希到字段元素。

仪式

为了保护 OAuth 构件的隐私,我们提供了对构件拥有零知识证明的证明。zkLogin 使用 Groth16 zkSNARK 来实例化零知识证明,因为它是就证明大小和验证效率而言最高效的通用 zkSNARK。

然而,Groth16 需要由可信方设置的特定于计算的公共参考字符串(CRS)。由于 zkLogin 预期确保高价值交易的安全保管和关键智能合约的完整性,我们不能将系统的安全性建立在单个实体的诚实上。因此,为了生成 zkLogin 电路的 CRS,必须运行一个基于大量参与方的假设诚实的协议。

什么是仪式?

Sui zkLogin 仪式本质上是由多样化的参与者执行的密码多方计算(MPC),以生成这个 CRS。我们遵循由 Bowe、Gabizon 和 Miers 描述的 MPC 协议 MMORPG。该协议大致分为两个阶段。第一个阶段导致在椭圆曲线元素的指数中得到一系列秘密量 tau 的幂。由于此阶段与电路无关,我们采用了现有社区贡献的 perpetual powers of tau 的结果。我们的仪式是第二阶段,专门用于 zkLogin 电路。

MMORPG 协议是一个顺序协议,允许无限数量的参与方按顺序参与,无需任何先前的同步或排序。每个参与方需要下载前一个参与方的输出,生成自己的熵,然后将其叠加在接收到的结果之上,产生自己的贡献,然后传递给下一个参与方。该协议保证安全性,只要至少有一个参与方忠实地遵循协议、生成强熵并可靠地丢弃它。

仪式如何进行?

我们向 100 多名具有不同背景和从属关系的人发送了邀请:Sui 验证人、密码学家、web3 专家、世界著名学者和商业领袖。我们计划在 2023 年 9 月 12 日至 18 日的日期进行仪式,但允许参与者在希望的任何时候加入,没有固定的时间段。

由于 MPC 是顺序的,每个贡献者都需要等到前一个贡献者完成,以便接收先前的贡献,按照 MPC 步骤进行,并生成自己的贡献。由于这个结构,我们提供了一个队列,参与者在其中等待,而那些在他们之前加入的人完成了。为了验证参与者,我们向每个参与者发送了一个唯一的激活代码。激活代码是签名密钥对的秘密密钥,具有双重目的:它允许协调服务器将参与者的电子邮件与贡献关联起来,并使用相应的公钥验证贡献。

参与者有两种贡献选项:通过浏览器或通过 Docker。浏览器选项对于贡献者在浏览器中完成所有操作更加用户友好。Docker 选项需要 Docker 设置,但更透明 —— Dockerfile 和贡献者源代码是开源的,并且整个过程是可验证的。此外,浏览器选项使用 snarkjs,而 Docker 选项使用 Kobi 的实现。这提供了软件的多样性,参与者可以选择通过他们信任的任何方法进行贡献。此外,参与者可以通过输入随机文本或进行随机光标移动来生成熵。

zkLogin 电路和仪式客户端 代码 被开源,链接在仪式之前提供给参与者进行审核。此外,我们还发布了这些开发人员文档和 zkSecurity 对电路的 审计报告。我们从第 1 阶段采用了 perpetual powers of tau挑战 #0081(由 80 个社区贡献产生),该阶段与电路无关。我们在第 2 阶段应用了 Drand 随机信标在纪元 #3298000 上的输出,以消除偏差。对于第 2 阶段,我们的仪式有 111 个贡献,其中浏览器贡献 82 个,Docker 贡献 29 个。最后,我们在第 2 阶段应用了 Drand 随机信标在纪元 #3320606 上的输出,以消除贡献的偏差。所有中间文件可以按照 这里 的说明进行第 1 阶段的复制,以及 这里 的说明进行第 2 阶段的复制。

最终化

最终的 CRS 以及每个参与者的贡献记录的成绩单都在一个公共存储库中可用。贡献者收到了他们正在工作的前一个贡献的哈希以及他们的贡献后的结果哈希,这两个哈希都在屏幕上显示并通过电子邮件发送。他们可以将这些哈希与在仪式站点上公开可用的记录进行比较。此外,任何人都可以检查哈希是否计算正确,每个贡献是否正确地纳入了最终的参数中。

最终,最终的 CRS 被用于生成证明密钥和验证密钥。证明密钥用于为 zkLogin 生成零知识证明,并与 ZK 证明服务一起存储。验证密钥已经 部署 作为验证人软件的一部分(协议版本 25 在 发布 1.10.1 中使用),用于验证 Sui 上的 zkLogin 交易。

安全和隐私

我们将逐步介绍所有 zkLogin 构件、它们的安全假设以及丢失或暴露的后果。

JWT 令牌

JWT 令牌的有效性受到客户端 ID(即 aud)的限制,以防止钓鱼攻击。对于证明的同源策略阻止了为恶意应用程序获取的 JWT 令牌用于 zkLogin。针对客户端 ID 获取的 JWT 令牌直接通过重定向 URL 发送到应用程序前端。特定客户端 ID 的泄漏的 JWT 令牌可能会损害用户隐私,因为这些令牌经常包含诸如用户名和电子邮件之类的敏感信息。此外,如果后端 salt 服务器负责用户 salt 管理,JWT 令牌可能会被利用以检索用户的 salt,从而引入额外的风险。

但是,JWT 泄漏并不意味着资金的损失,只要相应的短暂私钥是安全的。

用户 Salt

用户 salt 是获取对 zkLogin 钱包访问的必要条件。这个值对于 ZK 证明生成和 zkLogin 地址推导都是必不可少的。

用户 salt 泄漏并不意味着资金的损失,但它使攻击者能够将用户的主题标识符(即 sub)与 Sui 地址关联起来。这可能是有问题的,具体取决于是否使用了成对或公共主题标识符。特别是,如果使用成对的 ID(例如,Facebook),则主题标识符是每个 RP 唯一的。然而,对于可重用的公共 ID(例如,Google 和 Twitch),全球唯一的 sub 值可以用于识别用户。

短暂私钥

短暂的私钥寿命与用于创建有效的零知识证明的 nonce 中指定的最大纪元相关联。如果它被放错地方,可以为交易签名生成新的短暂私钥,同时使用新的 nonce 生成新的 ZK 证明。然而,如果短暂私钥被泄露,获取用户盐和有效的 ZK 证明将是必要的以移动资金。

证明

仅仅获得证明本身不能创建有效的 zkLogin 交易,因为还需要交易上的短暂签名。

隐私

默认情况下,OAuth 主题标识符(即 sub)与 Sui 地址之间没有链接。这就是用户盐的目的。 JWT 令牌默认情况下不会上链。揭示的值包括 issaudkid,以便可以计算公共输入哈希,任何敏感字段(如 sub)在生成证明时被用作私有输入。

零知识证明服务和盐服务(如果维护的话)可以链接用户身份,因为用户盐和 JWT 令牌是已知的,但这两个服务都是无状态设计。

将来,用户可以选择验证与 Sui 地址关联的他们的 OAuth 身份。

FAQ

zkLogin 兼容的提供商是哪些?

zkLogin 可以支持在 OAuth 2.0 框架之上构建的 OpenID Connect 的提供商。这是 OAuth 2.0 兼容提供商的子集。目前 Sui 支持 Google、Facebook 和 Twitch。其他兼容提供商将通过将来的协议升级启用。

zkLogin 钱包与传统私钥钱包有何不同?

传统私钥钱包要求用户始终记住助记词和口令,需要安全存储以防止由于私钥泄露而导致资金损失。

另一方面,zkLogin 钱包只需要短暂私钥存储与会话到期以及带有到期的 OAuth 登录流程。遗忘短暂私钥不会导致资金损失,因为用户随时可以重新登录以生成新的短暂私钥和新的 ZK 证明。

zkLogin 与 MPC 或多重签名钱包有何不同?

多方计算(MPC)和多重签名钱包依赖于多个密钥或分发多个密钥份额,然后定义接受签名的阈值。

zkLogin 不会分割任何个体私钥,而是在用户使用 OAuth 提供商进行身份验证时使用新的 nonce 注册短暂私钥。zkLogin 的主要优势在于用户不需要在任何地方管理任何持久的私钥,甚至不需要使用 MPC 或多重签名等私钥管理技术。

你可以将 zkLogin 视为地址的双因素身份验证方案,其中第一部分是用户的 OAuth 帐户,第二部分是用户的盐。

此外,由于 Sui 本地支持多重签名钱包,用户始终可以在多重签名钱包中包含一个或多个 zkLogin 签名者,以增加安全性,例如在 k-of-N 设置中将 zkLogin 部分用作双因素身份验证。

如果我的 OAuth 帐户被破坏,我的 zkLogin 地址会发生什么?

由于 zkLogin 是一个双因素身份验证系统,攻击者如果破坏了你的 OAuth 帐户,则无法访问你的 zkLogin 地址,除非他们单独破坏了你的盐。

如果我失去对我的 OAuth 帐户的访问权限,我会失去对我的 zkLogin 地址的访问权限吗?

是的。你必须能够登录到你的 OAuth 帐户并生成当前的 JWT,以便使用 zkLogin。

失去 OAuth 凭证是否意味着 zkLogin 钱包中的资金丧失?

通常可以通过在提供商中重置密码来找回遗忘的 OAuth 凭证。 在不幸的情况下,用户的 OAuth 凭证被破坏,对手仍然需要获取 user_salt,但还需要了解使用哪个钱包以接管该帐户。请注意,现代的 user_salt 提供商可能有额外的双因素身份验证安全措施,以防止向提供有效且未过期的 JWT 的实体提供用户盐。

同样重要的是要强调,由于 zkLogin 地址不暴露有关用户身份或使用的钱包的任何信息,仅通过监视区块链进行的有针对性的攻击更加困难。 最后,在不幸的情况下,如果永久失去对其 OAuth 帐户的访问权限,将失去对该钱包的访问权限。但如果希望从失去的 OAuth 帐户中恢复,对于钱包提供商的一个好建议是支持原生 Sui 多重签名功能并添加一种备份方法。请注意,甚至可以在所有签署者都使用 zkLogin 的多重签名钱包中使用,即一个 1-of-2 多重签名 zkLogin 钱包,其中第一部分是 Google,第二部分是 Facebook OAuth。

我能将传统私钥钱包转换或合并为 zkLogin 钱包,反之亦然吗?

不行。与私钥地址相比,zkLogin 钱包地址的派生方式不同。

我的 zkLogin 地址会改变吗?

zkLogin 地址是从 subissauduser_salt 派生的。

如果用户使用相同的 OAuth 提供商登录相同的钱包,则地址不会更改,因为在 JWT 令牌中,subissauduser_salt(参见定义)将保持不变,即使用户每次登录时 JWT 令牌本身可能看起来不同。

但是,如果用户使用不同的 OAuth 提供商登录,则地址将更改,因为issaud 是每个提供商分别定义的。

此外,每个钱包或应用程序都维护其自己的 user_salt,因此使用相同的提供商登录不同的钱包可能导致不同的地址。

有关地址的详细信息,请参见定义

我能够在相同的 OAuth 提供商下拥有多个地址吗?

是的,通过使用不同的钱包提供商或不同的 user_salt 为每个帐户,这是可能的。这对于在不同帐户之间分隔资金非常有用。

zkLogin 钱包是托管的吗?

zkLogin 钱包是非托管或非托管钱包。

托管或托管钱包是第三方(托管人)代表钱包用户控制私钥的地方。对于 zkLogin 钱包不存在这样的第三方。

相反,zkLogin 钱包可以被视为 2-of-2 多重签名,其中两个凭据分别是用户的 OAuth 凭据(由用户维护)和盐。换句话说,OAuth 提供商、钱包供应商、ZK 证明服务或盐服务提供商都不是托管者。

生成 ZK 证明很昂贵,每笔交易都需要生成新的证明吗?

不需要。仅当短暂密钥对过期时才需要生成证明。由于 nonce 由短暂公钥 (eph_pk) 和到期 (max_epoch) 定义,因此 ZK 证明有效直到在 JWT 令牌的 nonce 中承诺的到期。ZK 证明可以被缓存,可以使用相同的短暂密钥签署交易,直到它过期。

zkLogin 在移动设备上可以使用吗?

zkLogin 是 Sui 本地的原语,不是特定应用程序或钱包的功能。任何 Sui 开发人员都可以使用它,包括在移动设备上使用。

如果用户失去 OAuth 凭据,是否可以进行帐户恢复?

是的,用户可以按照 OAuth 提供商的恢复流程进行。短暂私钥可以被刷新,在完成新的 OAuth 登录流程后,用户可以获得新的 ZK 证明并使用刷新后的密钥签署交易。

zkLogin 电路的一些假设是什么?

由于 Groth16 的工作方式,我们对 JWT 中的几个字段施加了长度限制。一些受到长度限制的字段包括 aud、iss、JWT 的头部和有效负载。例如,zkLogin 目前只能处理长度不超过 120 的 aud 值(此值尚未最终确定)。总体而言,我们试图确保这些限制尽可能宽松。我们在查看了我们能够获取的尽可能多的 JWT 后决定了这些值。

zkLogin 与支持社交登录的其他解决方案有何不同?

虽然使用 Web2 凭据为 Web3 钱包提供社交登录并非新概念,但现有的解决方案有一个或多个信任假设:

  1. 信任不同的网络或方在区块链之外验证 Web2 凭据,通常涉及由受信任方在链上发布的 JWK 神谕。
  2. 信任某些方来管理持久私钥,无论是使用 MPC、阈值密码学还是安全隔离。
  3. 依赖智能合约(账户抽象)在链上验证 JWT 令牌并揭示隐私字段,或者在链上验证可能昂贵的 ZK 证明。

一些现有的部署解决方案依赖于这些假设之一。Web3Auth 和 DAuth 社交登录要求在 Web3Auth Auth Network 节点上部署自定义 OAuth 验证器以验证 JWT 令牌。Magic Wallet 和 Privy 也需要自定义 OAuth 身份颁发者和验证者来采用 DID 标准。所有这些解决方案仍然需要持久私钥管理,无论是通过像 AWS 这样的受信任方通过委托,Shamir Secret Sharing 还是 MPC。

zkLogin 带给 Sui 的主要差异因素有:

  1. Sui 的本地支持:与其他通用区块链解决方案不同,zkLogin 仅部署在 Sui 上。这意味着 zkLogin 交易可以与多重签名和赞助交易无缝结合。

  2. 无需额外信任的自托管:我们利用 JWT 令牌中的 nonce 字段来承诺短暂公钥,因此不需要与任何受信任方一起管理持久私钥。此外,JWK 本身是由验证者的利益池中的权益者协商同意的神谕,不需要信任任何权威来源。

  3. 完全隐私:只需提交 ZK 证明和短暂签名到链上,无需其他信息。

  4. 与现有身份提供商兼容:zkLogin 兼容采用 OpenID Connect 的提供商。无需信任除 OAuth 提供商本身以外的任何中间身份颁发者或验证者。

如何在链下验证 zkLogin 签名?

以下选项支持 zkLogin 签名,无论是交易数据还是个人消息。

  1. 使用 keytool:查看 keytool 的使用方法。
$SUI_BINARY keytool zk-login-sig-verify -h
  1. 使用自托管服务器端点:请参阅 zklogin-verifier 的用法。