MystenLabs / sui

Sui, a next-generation smart contract platform with high throughput, low latency, and an asset-oriented programming model powered by the Move programming language
https://sui.io
Apache License 2.0
6.05k stars 11.14k forks source link

Sui ZkLogin Bug #17970

Closed Oluwatunmise116 closed 1 week ago

Oluwatunmise116 commented 4 months ago

Steps to Reproduce Issue

Trying to sign a transaction using ephemeralKeyPair from zkLogin

const { bytes, signature: userSignature } = await txb.sign({
            client: SUI_CLIENT,
            signer: keypair,
        });
        const zkLoginSignature = await AuthService.generateZkLoginSignature(userSignature);

        const res = await SUI_CLIENT.executeTransactionBlock({
            transactionBlock: bytes,
            signature: zkLoginSignature,
        })

Expected Result

Expected the transaction block to get signed and not throw error

Actual Result

The actual outcome was an error:

code: -32002
type:  "ServerError"
message: "Invalid user signature: Required Signature from 0xa7af1b9a4df5b0108c0755f2054f1e9902fd8ce19150c906fcf65be7430ed4cd is absent [\"0x48f081c4e57fb6e44c8721c06ecf7d12b2ffc741039e497ed7ea2896b67b0586\"]" 
stack: "Error: Invalid user signature: Required Signature from 0xa7af1b9a4df5b0108c0755f2054f1e9902fd8ce19150c906fcf65be7430ed4cd is absent [\"0x48f081c4e57fb6e44c8721c06ecf7d12b2ffc741039e497ed7ea2896b67b0586\"]\n    at SuiHTTPTransport.reque

System Information

joyqvq commented 4 months ago

the zklogin address cannot be derived from the keypair. instead you should use https://docs.sui.io/guides/developer/cryptography/zklogin-integration#get-the-users-sui-address to get the sender address for the transaction.

you must sent the sender address explicitly using https://docs.sui.io/guides/developer/cryptography/zklogin-integration#assemble-the-zklogin-signature-and-submit-the-transaction instead of using txb.sign with the keypair. the latter assumes the sender derives from public key, but thats not the case for zklogin txn.

chinwei1989 commented 4 months ago

having the same issue here. followed exactly what mentioned by @joyqvq . the zklogin address was derrived from jwt and salt using jwtToAddress function. The sender address was set to transactionBlock before the signing. It just throw error message below. I notice the second address in bracket is different from the actual zklogin address . what could go wrong?

Error: Invalid user signature: Required Signature from 0xab15875c3f859bca03956e66605e19163d2c47c866f0a53351510595e09051d2 is absent ["0xb4914ba36922d3367ece64dafbf7f3289a96e6051c06576ae7e879e276cd568c"]

chinwei1989 commented 4 months ago

Screenshot 2024-05-31 at 1 29 04 AM Screenshot 2024-05-31 at 1 29 09 AM

getting some weird issue with getExtendedEphemeralPublicKey. it seems the function expect publicKey but the value type is Ed25519PublicKey?

Argument of type 'Ed25519PublicKey' is not assignable to parameter of type 'PublicKey'. Property 'verifyTransaction' is missing in type 'Ed25519PublicKey' but required in type 'PublicKey'.ts(2345)

joyqvq commented 4 months ago

having the same issue here. followed exactly what mentioned by @joyqvq . the zklogin address was derrived from jwt and salt using jwtToAddress function. The sender address was set to transactionBlock before the signing. It just throw error message below. I notice the second address in bracket is different from the actual zklogin address . what could go wrong?

Error: Invalid user signature: Required Signature from 0xab15875c3f859bca03956e66605e19163d2c47c866f0a53351510595e09051d2 is absent ["0xb4914ba36922d3367ece64dafbf7f3289a96e6051c06576ae7e879e276cd568c"]

this error means that you defined the sender address incorrectly as what sui interprets. if this is testing, can you paste me the base64 encoded tx bytes and the base64 encoded zklogin signature here? you can also send me at TG @joyqvq if you prefer to send them there.

chinwei1989 commented 4 months ago

@joyqvq , have pm you via TG

Ayomide57 commented 4 months ago

@joyqvq Hello, I am having the same issues here, why is the sender different from the signer

joyqvq commented 4 months ago

you can refer to my answer from above. the zklogin address has to be derived with the zklogin sdk and set correctly to the transaction sender.

if you need more help, please post your relavant code, tx bytes and zklogin signature, and entire error string.

Ayomide57 commented 4 months ago

@joyqvq I honestly think it is impossible to use zklogin, I was able to resolve the previous issue but this is the new issue

"Error: Invalid user signature: Signature is not valid: Signature is not valid: General cryptographic error: Groth16 proof verify failed"

Here is my MoveCall function ` private async makeMoveCall(txData: any, txb: TransactionBlock) { const keypair = AuthService.getEd25519Keypair(); const sender = AuthService.walletAddress();

    console.log("zkLoginSignature ============", sender);
    txb.setSender(sender);
    txb.setGasBudget(100000000)
    const [coin] = txb.moveCall(txData);

   const { bytes, signature: userSignature } = await txb.sign({
        client: SUI_CLIENT,
        signer: keypair,
    });
    coin && txb.transferObjects([coin], sender);
    const zkLoginSignature = await AuthService.generateZkLoginSignature(userSignature);

    console.log("zkLoginSignature ============", zkLoginSignature);
    const transaction1 = SUI_CLIENT.executeTransactionBlock({
        transactionBlock: bytes,
        signature: zkLoginSignature,
    });

    console.log("Create profile ======", transaction1);
    return transaction1;
}

`

Here is my Auth Services `import axios from "axios"; import { SUI_CLIENT } from "./suiClient"; import { Ed25519Keypair } from '@mysten/sui.js/keypairs/ed25519'; import { generateNonce, generateRandomness, getExtendedEphemeralPublicKey } from '@mysten/zklogin'; import { jwtToAddress } from '@mysten/zklogin'; import { genAddressSeed, getZkLoginSignature } from "@mysten/zklogin"; import { jwtDecode } from "jwt-decode"; import { SerializedSignature } from "@mysten/sui.js/cryptography";

const PROVER_URL = process.env.NEXT_PUBLIC_PROVER_URL || ''; const REDIRECT_URL = process.env.NEXT_PUBLIC_REDIRECT_URL || ''; const OPENID_PROVIDER_URL = process.env.NEXT_PUBLIC_OPENID_PROVIDER_URL || ''; const CLIENT_ID = process.env.NEXT_PUBLIC_CLIENT_ID;

export class AuthService {

static getAddressSeed() {
    const jwt = AuthService.decodeJwt();
    const salt = AuthService.salt();
    if(salt && jwt)
        return genAddressSeed(BigInt(salt), "sub", jwt.sub || '', jwt.aud && jwt.aud.toString() || '').toString();
}

static getEd25519Keypair(): Ed25519Keypair {
    const jwtData = AuthService.getJwtData();
    const publicKey = new Uint8Array(Object.values(jwtData.ephemeralKeyPair.keypair.publicKey));
    const secretKey = new Uint8Array(Object.values(jwtData.ephemeralKeyPair.keypair.secretKey));
    return new Ed25519Keypair({ publicKey, secretKey })
}

static async getPartialZkLoginSignature(): Promise<any> {
    const keyPair = AuthService.getEd25519Keypair();
    const extendedEphemeralPublicKey = getExtendedEphemeralPublicKey(keyPair.getPublicKey());
    const verificationPayload = {
        jwt: AuthService.jwt(),
        extendedEphemeralPublicKey,
        maxEpoch: this.getMaxEpoch(),
        jwtRandomness: this.getRandomness(),
        salt: AuthService.salt(),
        keyClaimName: "sub"
    };
    return await AuthService.verifyPartialZkLoginSignature(verificationPayload);
}

private static async verifyPartialZkLoginSignature(zkpRequestPayload: any) {
    try {
        //console.log("failed to reqeust the partial sig ====================");
        const proofResponse = await axios.post(PROVER_URL, zkpRequestPayload, {
            headers: {
                'content-type': 'application/json',
                'token': AuthService.jwt()
            },
        });
        const partialZkLoginSignature = proofResponse.data as PartialZkLoginSignature;
        //console.log(proofResponse.data.proofPoints);
        return partialZkLoginSignature;
    } catch (error) {
        console.log("failed to reqeust the partial sig: ", error);
        return {};
    }
}

static async generateZkLoginSignature(userSignature: string): Promise<SerializedSignature> {
    const partialZkLoginSignature = await AuthService.getPartialZkLoginSignature();
    const addressSeed = AuthService.getAddressSeed();
    const maxEpoch = AuthService.getMaxEpoch();
    console.log("maxEpoch =========================", maxEpoch);
    return getZkLoginSignature({
        inputs: {
            ...partialZkLoginSignature,
            addressSeed
        },
        maxEpoch,
        userSignature,
    });
}

static getMaxEpoch() {
    return AuthService.getJwtData().maxEpoch;
}

static getRandomness() {
    return AuthService.getJwtData().randomness;
}

private static getJwtData() {
    return JSON.parse(window.sessionStorage.getItem("jwt_data") || '');
}

private static decodeJwt(): JwtPayload {
    const jwt = window.sessionStorage.getItem('sui_jwt_token');
    return jwtDecode(jwt || '') as JwtPayload;
}

private static salt() {
    //const email = AuthService.claims()['email'];
    //return AuthService.hashcode(email);
    const salt = sessionStorage.getItem("jwt_salt_data");
    return salt;
}

static walletAddress() {
    //const email = AuthService.claims()['email'];
    //return jwtToAddress(AuthService.jwt() || '', AuthService.hashcode(email));
    const jwt = AuthService.jwt();
    const salt = AuthService.salt();
    const zkLoginUserAddress = jwtToAddress(jwt || '', salt || '');
    return zkLoginUserAddress
}

private static claims() {
    const token = AuthService.jwt();
    if (token)
        return JSON.parse(atob(token.split('.')[1]));
}

private static hashcode(s: string) {
    var h = 0, l = s.length, i = 0;
    if (l > 0)
        while (i < l)
            h = (h << 5) - h + s.charCodeAt(i++) | 0;
    return h.toString();
}

static isAuthenticated() {
    const token = AuthService.jwt();
    return token && token !== 'null';
}

static jwt() {
    if (typeof window !== "undefined") return window.sessionStorage.getItem("sui_jwt_token");
}

static getZkUserAddress() {
    const jwt = AuthService.jwt();
    const salt = AuthService.salt();
    const zkLoginUserAddress = jwtToAddress(jwt || '', salt || '');
    return zkLoginUserAddress
}

async login() {

    const { epoch, epochDurationMs, epochStartTimestampMs } = await SUI_CLIENT.getLatestSuiSystemState();
    const maxEpoch = Number(epoch) + 2;
    const ephemeralKeyPair = new Ed25519Keypair();
    const randomness = generateRandomness();
    const nonce = generateNonce(ephemeralKeyPair.getPublicKey(), maxEpoch, randomness);
    const jwtData = {
        maxEpoch,
        nonce,
        randomness,
        ephemeralKeyPair,
    };
    sessionStorage.setItem("jwt_salt_data", generateRandomness());

    sessionStorage.setItem("jwt_data", JSON.stringify(jwtData));

    const params = new URLSearchParams({
        client_id: CLIENT_ID || '',
        redirect_uri: REDIRECT_URL,
        response_type: 'id_token',
        scope: 'openid email',
        nonce: nonce,
    });

    try {
        const { data } = await axios.get(OPENID_PROVIDER_URL);
        const authUrl = `${data.authorization_endpoint}?${params}`;
        window.location.href = authUrl;
    } catch (error) {
        console.error('Error initiating Google login:', error);
    }

}

} export interface JwtPayload { iss?: string; sub?: string; aud?: string[] | string; exp?: number; nbf?: number; iat?: number; jti?: string; }

export type PartialZkLoginSignature = Omit< Parameters["0"]["inputs"], "addressSeed"

; `

blockchainBard101 commented 1 week ago

@joyqvq Hello, I am having the same issues here, why is the sender different from the signer

I'm having the same issue

blockchainBard101 commented 1 week ago

@joyqvq i'm having same issues Signature is not valid: Signature is not valid: General cryptographic error: Groth16 proof verify failed

Screenshot 2024-09-26 180446

joyqvq commented 1 week ago

what prover URL you are using? if you are using prover-dev but submitting transactions to testnet or mainnet, the proof will not verify.

to use testnet or mainnet for zklogin, please refer to https://enoki.mystenlabs.com/