From b20adf5135e50299e1d01596a382b73083860fa6 Mon Sep 17 00:00:00 2001 From: Oliver He Date: Tue, 24 Sep 2024 12:56:37 -0400 Subject: [PATCH] Add support for federated keyless (#495) * Add support for federated keyless * update * federated local * add example * undup code * fmt * update examples * update tests * revert endpoints * update util * add todo * update jwk update * address comments * update * update * update ternary * fix async proof submission * fixes * fmt * update changelog * fix examples * fund account * fix test --- CHANGELOG.md | 1 + examples/typescript/federated_keyless.ts | 124 +++++++ examples/typescript/jwk_update.ts | 68 ++++ examples/typescript/keyless.ts | 12 +- examples/typescript/keyless_mainnet.ts | 6 +- examples/typescript/move/update_jwk/Move.toml | 15 + examples/typescript/move/update_jwk/script.mv | Bin 0 -> 486 bytes .../move/update_jwk/scripts/update_jwk.move | 25 ++ examples/typescript/package.json | 2 + examples/typescript/utils.ts | 11 + src/account/FederatedKeylessAccount.ts | 115 ++++++ src/account/KeylessAccount.ts | 297 +--------------- src/account/KeylessAccountCommon.ts | 329 ++++++++++++++++++ src/account/index.ts | 2 + src/api/keyless.ts | 54 ++- src/core/crypto/federatedKeyless.ts | 134 +++++++ src/core/crypto/index.ts | 1 + src/core/crypto/singleKey.ts | 6 + src/internal/keyless.ts | 90 ++++- src/internal/transactionSubmission.ts | 4 +- .../transactionBuilder/transactionBuilder.ts | 39 ++- src/types/index.ts | 1 + tests/e2e/api/keyless.test.ts | 185 +++++++--- 23 files changed, 1144 insertions(+), 377 deletions(-) create mode 100644 examples/typescript/federated_keyless.ts create mode 100644 examples/typescript/jwk_update.ts create mode 100644 examples/typescript/move/update_jwk/Move.toml create mode 100644 examples/typescript/move/update_jwk/script.mv create mode 100644 examples/typescript/move/update_jwk/scripts/update_jwk.move create mode 100644 src/account/FederatedKeylessAccount.ts create mode 100644 src/account/KeylessAccountCommon.ts create mode 100644 src/core/crypto/federatedKeyless.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ba0c8c24..022db93ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T - Populate `coinType` for `getAccountCoinAmount` if only `faMetadataAddress` is provided. - [`Fix`] `getModuleEventsByEventType` will also account for EventHandle events. - [`Hot Fix`] change regex to find object address when using `createObjectAndPublishPackage` move function +- Add support for federated keyless accounts as defined in AIP-96 # 1.27.1 (2024-08-23) diff --git a/examples/typescript/federated_keyless.ts b/examples/typescript/federated_keyless.ts new file mode 100644 index 000000000..9cddcaadb --- /dev/null +++ b/examples/typescript/federated_keyless.ts @@ -0,0 +1,124 @@ +/* eslint-disable max-len */ +/* eslint-disable no-console */ + +/** + * This example shows how to use the Federated Keyless accounts on Aptos + */ + +import { Account, AccountAddress, Aptos, AptosConfig, EphemeralKeyPair, Network } from "@aptos-labs/ts-sdk"; +import * as readlineSync from "readline-sync"; + +const ALICE_INITIAL_BALANCE = 100_000_000; +const BOB_INITIAL_BALANCE = 100_000_000; +const TRANSFER_AMOUNT = 10_000; + +/** + * Prints the balance of an account + * @param aptos + * @param name + * @param address + * @returns {Promise<*>} + * + */ +const balance = async (aptos: Aptos, name: string, address: AccountAddress) => { + const amount = await aptos.getAccountAPTAmount({ + accountAddress: address, + }); + console.log(`${name}'s balance is: ${amount}`); + return amount; +}; + +const example = async () => { + // Setup the client + const config = new AptosConfig({ network: Network.DEVNET }); + const aptos = new Aptos(config); + + // Generate the ephemeral (temporary) key pair that will be used to sign transactions. + const ephemeralKeyPair = EphemeralKeyPair.generate(); + + console.log("\n=== Federated Keyless Account Example ===\n"); + + const link = `https://dev-qtdgjv22jh0v1k7g.us.auth0.com/authorize?client_id=dzqI77x0M5YwdOSUx6j25xkdOt8SIxeE&redirect_uri=http%3A%2F%2Flocalhost%3A5173%2Fcallback&response_type=id_token&scope=openid&nonce=${ephemeralKeyPair.nonce}`; + console.log(`${link}\n`); + + console.log("1. Open the link above"); + console.log("2. Log in with your Google account"); + console.log("3. Click 'Exchange authorization code for tokens"); + console.log("4. Copy the 'id_token' - (toggling 'Wrap lines' option at the bottom makes this easier)\n"); + + function inputJwt(): string { + const jwt: string = readlineSync.question("Paste the JWT (id_token) token here and press enter:\n\n", { + hideEchoBack: false, + }); + return jwt; + } + + const jwt = inputJwt(); + + const bob = Account.generate(); + + // Derive the Keyless Account from the JWT and ephemeral key pair. + const alice = await aptos.deriveKeylessAccount({ + jwt, + ephemeralKeyPair, + jwkAddress: bob.accountAddress, + }); + + console.log("\n=== Addresses ===\n"); + console.log(`Alice's keyless account address is: ${alice.accountAddress}`); + console.log(`Alice's nonce is: ${ephemeralKeyPair.nonce}`); + console.log(`Alice's ephem pubkey is: ${ephemeralKeyPair.getPublicKey().toString()}`); + console.log(`\nBob's account address is: ${bob.accountAddress}`); + + // Fund the accounts + console.log("\n=== Funding accounts ===\n"); + + await aptos.fundAccount({ + accountAddress: alice.accountAddress, + amount: ALICE_INITIAL_BALANCE, + options: { waitForIndexer: false }, + }); + await aptos.fundAccount({ + accountAddress: bob.accountAddress, + amount: BOB_INITIAL_BALANCE, + options: { waitForIndexer: false }, + }); + + // // Show the balances + console.log("\n=== Balances ===\n"); + const aliceBalance = await balance(aptos, "Alice", alice.accountAddress); + const bobBalance = await balance(aptos, "Bob", bob.accountAddress); + + const iss = "https://dev-qtdgjv22jh0v1k7g.us.auth0.com/"; + + console.log("\n=== Installing JWKs ===\n"); + const jwkTxn = await aptos.updateFederatedKeylessJwkSetTransaction({ sender: bob, iss }); + const committedJwkTxn = await aptos.signAndSubmitTransaction({ signer: bob, transaction: jwkTxn }); + await aptos.waitForTransaction({ transactionHash: committedJwkTxn.hash }); + console.log(`Committed transaction: ${committedJwkTxn.hash}`); + + // Transfer between users + const transaction = await aptos.transferCoinTransaction({ + sender: alice.accountAddress, + recipient: bob.accountAddress, + amount: TRANSFER_AMOUNT, + }); + + console.log("\n=== Transferring ===\n"); + const committedTxn = await aptos.signAndSubmitTransaction({ signer: alice, transaction }); + + await aptos.waitForTransaction({ transactionHash: committedTxn.hash }); + console.log(`Committed transaction: ${committedTxn.hash}`); + + console.log("\n=== Balances after transfer ===\n"); + const newAliceBalance = await balance(aptos, "Alice", alice.accountAddress); + const newBobBalance = await balance(aptos, "Bob", bob.accountAddress); + + // Bob should have the transfer amount minus gas to insert jwk + if (TRANSFER_AMOUNT <= newBobBalance - bobBalance) throw new Error("Bob's balance after transfer is incorrect"); + + // Alice should have the remainder minus gas + if (TRANSFER_AMOUNT >= aliceBalance - newAliceBalance) throw new Error("Alice's balance after transfer is incorrect"); +}; + +example(); diff --git a/examples/typescript/jwk_update.ts b/examples/typescript/jwk_update.ts new file mode 100644 index 000000000..5fbc978c7 --- /dev/null +++ b/examples/typescript/jwk_update.ts @@ -0,0 +1,68 @@ +/* eslint-disable max-len */ +/* eslint-disable no-console */ + +/** + * This example shows how to use install JWKs on an account to support Federated Keyless Accounts + */ + +import { Aptos, AptosConfig, EphemeralKeyPair, Network } from "@aptos-labs/ts-sdk"; +import * as readlineSync from "readline-sync"; + +const example = async () => { + const config = new AptosConfig({ network: Network.DEVNET }); + const aptos = new Aptos(config); + + // Generate the ephemeral (temporary) key pair that will be used to sign transactions. + const ephemeralKeyPair = EphemeralKeyPair.generate(); + + console.log("\n=== Federated Keyless JWK Installation ===\n"); + + const link = `https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?redirect_uri=https%3A%2F%2Fdevelopers.google.com%2Foauthplayground&prompt=consent&response_type=code&client_id=407408718192.apps.googleusercontent.com&scope=openid&access_type=offline&service=lso&o2v=2&theme=glif&flowName=GeneralOAuthFlow&nonce=${ephemeralKeyPair.nonce}`; + console.log(`${link}\n`); + + console.log("1. Open the link above"); + console.log("2. Log in with your Google account"); + console.log("3. Click 'Exchange authorization code for tokens"); + console.log("4. Copy the 'id_token' - (toggling 'Wrap lines' option at the bottom makes this easier)\n"); + + function inputJwt(): string { + const jwt: string = readlineSync.question("Paste the JWT (id_token) token here and press enter:\n\n", { + hideEchoBack: false, + }); + return jwt; + } + + function inputIss(): string { + const jwt: string = readlineSync.question( + "\nInput the iss claim of your federated OIDC provider and press enter (e.g. https://dev-qtdgjv22jh0v1k7g.us.auth0.com/):\n\n", + { + hideEchoBack: false, + }, + ); + return jwt; + } + + const jwt = inputJwt(); + const iss = inputIss(); + + const alice = await aptos.deriveKeylessAccount({ + jwt, + ephemeralKeyPair, + }); + await aptos.fundAccount({ + accountAddress: alice.accountAddress, + amount: 100_000_000, + }); + + const jwkTxn = await aptos.updateFederatedKeylessJwkSetTransaction({ sender: alice, iss }); + await aptos.signAndSubmitTransaction({ signer: alice, transaction: jwkTxn }); + + console.log("\n=== Addresses ===\n"); + console.log(`JWKs were installed at - ${alice.accountAddress}\n`); + console.log("Use it to construct Federated Keyless Accounts for your federated JWT tokens\n\n"); + console.log( + `await aptos.deriveKeylessAccount({\n jwt,\n ephemeralKeyPair,\n jwkAddress: "${alice.accountAddress}",\n});`, + ); +}; + +example(); diff --git a/examples/typescript/keyless.ts b/examples/typescript/keyless.ts index 87d4f14f0..269ae9022 100644 --- a/examples/typescript/keyless.ts +++ b/examples/typescript/keyless.ts @@ -8,7 +8,6 @@ import { Account, AccountAddress, Aptos, AptosConfig, EphemeralKeyPair, Network } from "@aptos-labs/ts-sdk"; import * as readlineSync from "readline-sync"; -const COIN_STORE = "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>"; const ALICE_INITIAL_BALANCE = 100_000_000; const BOB_INITIAL_BALANCE = 100; const TRANSFER_AMOUNT = 10_000; @@ -22,20 +21,17 @@ const TRANSFER_AMOUNT = 10_000; * */ const balance = async (aptos: Aptos, name: string, address: AccountAddress) => { - type Coin = { coin: { value: string } }; - const resource = await aptos.getAccountResource({ + const amount = await aptos.getAccountAPTAmount({ accountAddress: address, - resourceType: COIN_STORE, }); - const amount = Number(resource.coin.value); - console.log(`${name}'s balance is: ${amount}`); return amount; }; const example = async () => { // Setup the client - const config = new AptosConfig({ network: Network.DEVNET }); + const network = Network.DEVNET; + const config = new AptosConfig({ network }); const aptos = new Aptos(config); // Generate the ephemeral (temporary) key pair that will be used to sign transactions. @@ -101,7 +97,7 @@ const example = async () => { const committedTxn = await aptos.signAndSubmitTransaction({ signer: alice, transaction }); await aptos.waitForTransaction({ transactionHash: committedTxn.hash }); - console.log(`Committed transaction: ${committedTxn.hash}`); + console.log(`\nCommitted transaction:\nhttps://explorer.aptoslabs.com/txn/${committedTxn.hash}?network=${network}`); console.log("\n=== Balances after transfer ===\n"); const newAliceBalance = await balance(aptos, "Alice", alice.accountAddress); diff --git a/examples/typescript/keyless_mainnet.ts b/examples/typescript/keyless_mainnet.ts index 759caf84c..599083608 100644 --- a/examples/typescript/keyless_mainnet.ts +++ b/examples/typescript/keyless_mainnet.ts @@ -8,7 +8,6 @@ import { AccountAddress, Aptos, AptosConfig, EphemeralKeyPair, Network } from "@aptos-labs/ts-sdk"; import * as readlineSync from "readline-sync"; -const COIN_STORE = "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>"; const TRANSFER_AMOUNT = 10; /** @@ -19,12 +18,9 @@ const TRANSFER_AMOUNT = 10; * */ const balance = async (aptos: Aptos, address: AccountAddress) => { - type Coin = { coin: { value: string } }; - const resource = await aptos.getAccountResource({ + const amount = await aptos.getAccountAPTAmount({ accountAddress: address, - resourceType: COIN_STORE, }); - const amount = Number(resource.coin.value); return amount; }; diff --git a/examples/typescript/move/update_jwk/Move.toml b/examples/typescript/move/update_jwk/Move.toml new file mode 100644 index 000000000..81eb6feae --- /dev/null +++ b/examples/typescript/move/update_jwk/Move.toml @@ -0,0 +1,15 @@ +[package] +name = "update_jwk" +version = "1.0.0" +authors = [] + +[addresses] + +[dev-addresses] + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-core.git" +rev = "devnet" +subdir = "aptos-move/framework/aptos-framework" + +[dev-dependencies] diff --git a/examples/typescript/move/update_jwk/script.mv b/examples/typescript/move/update_jwk/script.mv new file mode 100644 index 0000000000000000000000000000000000000000..7cc7a7f187bcb379d94350ac5f33ad2452278e65 GIT binary patch literal 486 zcmaJ+%Sr=55bW-G?Cd-O8^nX;5HAWrJ$UkHKoQhKz=OwSNk)z4#qP%F=lFf%KiHdy zn2S9#MNd_AH~rE1bQ%B>1WnqD_MSW?wSwSEa34r9M5+E%kvR`qPNen=m za%QapmbABI4g6kXfDbk(3@gMbYMTNgpdq=ISww?miGdq1{k(dqbXAw**`t1q!+Kuo zyY)|8-`}V~Q4gQYxXR;nQNQoZ;woP>R9?pE{59sqWa1jKtcsjF{V^;TRV?c@xv6-F zqgWPo95r0U%l;RUfdfkNvPkee$j)`efa{Y^&XERc`o>>TdI@PmcWu zs<*{wc&>TaI(*nnyj7=xF3G1sFzO%~lb{%_OPzL!vl)3~(V0wmAA<82S{*xZA(QkC DUUEJf literal 0 HcmV?d00001 diff --git a/examples/typescript/move/update_jwk/scripts/update_jwk.move b/examples/typescript/move/update_jwk/scripts/update_jwk.move new file mode 100644 index 000000000..5c1b73081 --- /dev/null +++ b/examples/typescript/move/update_jwk/scripts/update_jwk.move @@ -0,0 +1,25 @@ +script { + use aptos_framework::jwks; + use std::string::String; + use std::vector; + fun main(account: &signer, iss: vector, kid_vec: vector, alg_vec: vector, e_vec: vector, n_vec: vector) { + assert!(!vector::is_empty(&kid_vec), 0); + let num_jwk = vector::length(&kid_vec); + assert!(vector::length(&alg_vec) == num_jwk , 0); + assert!(vector::length(&e_vec) == num_jwk, 0); + assert!(vector::length(&n_vec) == num_jwk, 0); + + let remove_all_patch = jwks::new_patch_remove_all(); + let patches = vector[remove_all_patch]; + while (!vector::is_empty(&kid_vec)) { + let kid = vector::pop_back(&mut kid_vec); + let alg = vector::pop_back(&mut alg_vec); + let e = vector::pop_back(&mut e_vec); + let n = vector::pop_back(&mut n_vec); + let jwk = jwks::new_rsa_jwk(kid, alg, e, n); + let patch = jwks::new_patch_upsert_jwk(iss, jwk); + vector::push_back(&mut patches, patch) + }; + jwks::patch_federated_jwks(account, patches); + } +} \ No newline at end of file diff --git a/examples/typescript/package.json b/examples/typescript/package.json index f65df4274..7b186df56 100644 --- a/examples/typescript/package.json +++ b/examples/typescript/package.json @@ -16,6 +16,8 @@ "external_signing": "ts-node external_signing.ts", "your_coin": "ts-node your_coin.ts", "your_fungible_asset": "ts-node your_fungible_asset.ts", + "federated_keyless": "ts-node federated_keyless.ts", + "jwk_update": "ts-node jwk_update.ts", "keyless": "ts-node keyless.ts", "keyless_mainnet": "ts-node keyless_mainnet.ts", "local_node": "ts-node local_node.ts", diff --git a/examples/typescript/utils.ts b/examples/typescript/utils.ts index 4a96f89b0..8fa073f28 100644 --- a/examples/typescript/utils.ts +++ b/examples/typescript/utils.ts @@ -52,3 +52,14 @@ export function getPackageBytesToPublish(filePath: string) { return { metadataBytes, byteCode }; } + +/** + * A convenience function to get a scripts byteCode + * @param filePath a path relative to the current working directory + */ +export function getMoveBytes(filePath: string) { + const cwd = process.cwd(); + const modulePath = path.join(cwd, filePath); + const buffer = fs.readFileSync(modulePath); + return Uint8Array.from(buffer); +} diff --git a/src/account/FederatedKeylessAccount.ts b/src/account/FederatedKeylessAccount.ts new file mode 100644 index 000000000..a605c968e --- /dev/null +++ b/src/account/FederatedKeylessAccount.ts @@ -0,0 +1,115 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { JwtPayload, jwtDecode } from "jwt-decode"; +import { HexInput } from "../types"; +import { AccountAddress, AccountAddressInput } from "../core/accountAddress"; +import { ZeroKnowledgeSig } from "../core/crypto"; + +import { EphemeralKeyPair } from "./EphemeralKeyPair"; +import { Deserializer, Serializer } from "../bcs"; +import { FederatedKeylessPublicKey } from "../core/crypto/federatedKeyless"; +import { KeylessAccountCommon, ProofFetchCallback } from "./KeylessAccountCommon"; + +/** + * Account implementation for the FederatedKeyless authentication scheme. + * + * Used to represent a FederatedKeyless based account and sign transactions with it. + * + * Use FederatedKeylessAccount.fromJWTAndProof to instantiate a KeylessAccount with a JWT, proof, EphemeralKeyPair and the + * address the JWKs are installed that will be used to verify the JWT. + * + * When the proof expires or the JWT becomes invalid, the KeylessAccount must be instantiated again with a new JWT, + * EphemeralKeyPair, and corresponding proof. + */ +export class FederatedKeylessAccount extends KeylessAccountCommon { + /** + * The FederatedKeylessPublicKey associated with the account + */ + readonly publicKey: FederatedKeylessPublicKey; + + // Use the static constructor 'create' instead. + private constructor(args: { + address?: AccountAddress; + ephemeralKeyPair: EphemeralKeyPair; + iss: string; + uidKey: string; + uidVal: string; + aud: string; + pepper: HexInput; + jwkAddress: AccountAddress; + proof: ZeroKnowledgeSig | Promise; + proofFetchCallback?: ProofFetchCallback; + jwt: string; + }) { + super(args); + this.publicKey = FederatedKeylessPublicKey.create(args); + } + + serialize(serializer: Serializer): void { + if (this.proof === undefined) { + throw new Error("Connot serialize - proof undefined"); + } + serializer.serializeStr(this.jwt); + serializer.serializeStr(this.uidKey); + serializer.serializeFixedBytes(this.pepper); + this.publicKey.jwkAddress.serialize(serializer); + this.ephemeralKeyPair.serialize(serializer); + this.proof.serialize(serializer); + } + + static deserialize(deserializer: Deserializer): FederatedKeylessAccount { + const jwt = deserializer.deserializeStr(); + const uidKey = deserializer.deserializeStr(); + const pepper = deserializer.deserializeFixedBytes(31); + const jwkAddress = AccountAddress.deserialize(deserializer); + const ephemeralKeyPair = EphemeralKeyPair.deserialize(deserializer); + const proof = ZeroKnowledgeSig.deserialize(deserializer); + return FederatedKeylessAccount.create({ + proof, + pepper, + jwkAddress, + uidKey, + jwt, + ephemeralKeyPair, + }); + } + + static fromBytes(bytes: Uint8Array): FederatedKeylessAccount { + return FederatedKeylessAccount.deserialize(new Deserializer(bytes)); + } + + static create(args: { + address?: AccountAddress; + proof: ZeroKnowledgeSig | Promise; + jwt: string; + ephemeralKeyPair: EphemeralKeyPair; + pepper: HexInput; + jwkAddress: AccountAddressInput; + uidKey?: string; + proofFetchCallback?: ProofFetchCallback; + }): FederatedKeylessAccount { + const { address, proof, jwt, ephemeralKeyPair, pepper, jwkAddress, uidKey = "sub", proofFetchCallback } = args; + + const jwtPayload = jwtDecode(jwt); + const iss = jwtPayload.iss!; + if (typeof jwtPayload.aud !== "string") { + throw new Error("aud was not found or an array of values"); + } + const aud = jwtPayload.aud!; + const uidVal = jwtPayload[uidKey]; + return new FederatedKeylessAccount({ + address, + proof, + ephemeralKeyPair, + iss, + uidKey, + uidVal, + aud, + pepper, + jwkAddress: AccountAddress.from(jwkAddress), + jwt, + proofFetchCallback, + }); + } +} diff --git a/src/account/KeylessAccount.ts b/src/account/KeylessAccount.ts index b73950e43..7984060df 100644 --- a/src/account/KeylessAccount.ts +++ b/src/account/KeylessAccount.ts @@ -2,27 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import { JwtPayload, jwtDecode } from "jwt-decode"; -import EventEmitter from "eventemitter3"; -import { EphemeralCertificateVariant, HexInput, SigningScheme } from "../types"; +import { HexInput } from "../types"; import { AccountAddress } from "../core/accountAddress"; -import { - AnyPublicKey, - AnySignature, - KeylessPublicKey, - KeylessSignature, - EphemeralCertificate, - ZeroKnowledgeSig, - ZkProof, -} from "../core/crypto"; +import { ZeroKnowledgeSig } from "../core/crypto"; -import { Account } from "./Account"; import { EphemeralKeyPair } from "./EphemeralKeyPair"; -import { Hex } from "../core/hex"; -import { AccountAuthenticatorSingleKey } from "../transactions/authenticator/account"; -import { Deserializer, Serializable, Serializer } from "../bcs"; -import { deriveTransactionType, generateSigningMessage } from "../transactions/transactionBuilder/signingMessage"; -import { AnyRawTransaction, AnyRawTransactionInstance } from "../transactions/types"; -import { base64UrlDecode } from "../utils/helpers"; +import { Deserializer, Serializer } from "../bcs"; +import { KeylessAccountCommon, ProofFetchCallback } from "./KeylessAccountCommon"; /** * Account implementation for the Keyless authentication scheme. @@ -34,71 +20,7 @@ import { base64UrlDecode } from "../utils/helpers"; * When the proof expires or the JWT becomes invalid, the KeylessAccount must be instantiated again with a new JWT, * EphemeralKeyPair, and corresponding proof. */ -export class KeylessAccount extends Serializable implements Account { - static readonly PEPPER_LENGTH: number = 31; - - /** - * The KeylessPublicKey associated with the account - */ - readonly publicKey: KeylessPublicKey; - - /** - * The EphemeralKeyPair used to generate sign. - */ - readonly ephemeralKeyPair: EphemeralKeyPair; - - /** - * The claim on the JWT to identify a user. This is typically 'sub' or 'email'. - */ - readonly uidKey: string; - - /** - * The value of the uidKey claim on the JWT. This intended to be a stable user identifier. - */ - readonly uidVal: string; - - /** - * The value of the 'aud' claim on the JWT, also known as client ID. This is the identifier for the dApp's - * OIDC registration with the identity provider. - */ - readonly aud: string; - - /** - * A value contains 31 bytes of entropy that preserves privacy of the account. Typically fetched from a pepper provider. - */ - readonly pepper: Uint8Array; - - /** - * Account address associated with the account - */ - readonly accountAddress: AccountAddress; - - /** - * The zero knowledge signature (if ready) which contains the proof used to validate the EphemeralKeyPair. - */ - proof: ZeroKnowledgeSig | undefined; - - /** - * The proof of the EphemeralKeyPair or a promise that provides the proof. This is used to allow for awaiting on - * fetching the proof. - */ - readonly proofOrPromise: ZeroKnowledgeSig | Promise; - - /** - * Signing scheme used to sign transactions - */ - readonly signingScheme: SigningScheme; - - /** - * The JWT token used to derive the account - */ - readonly jwt: string; - - /** - * An event emitter used to assist in handling asycronous proof fetching. - */ - private readonly emitter: EventEmitter; - +export class KeylessAccount extends KeylessAccountCommon { // Use the static constructor 'create' instead. private constructor(args: { address?: AccountAddress; @@ -112,52 +34,7 @@ export class KeylessAccount extends Serializable implements Account { proofFetchCallback?: ProofFetchCallback; jwt: string; }) { - super(); - const { address, ephemeralKeyPair, uidKey, uidVal, aud, pepper, proof, proofFetchCallback, jwt } = args; - this.ephemeralKeyPair = ephemeralKeyPair; - this.publicKey = KeylessPublicKey.create(args); - this.accountAddress = address ? AccountAddress.from(address) : this.publicKey.authKey().derivedAddress(); - this.uidKey = uidKey; - this.uidVal = uidVal; - this.aud = aud; - this.jwt = jwt; - this.emitter = new EventEmitter(); - this.proofOrPromise = proof; - if (proof instanceof ZeroKnowledgeSig) { - this.proof = proof; - } else { - if (proofFetchCallback === undefined) { - throw new Error("Must provide callback for async proof fetch"); - } - this.emitter.on("proofFetchFinish", async (status) => { - await proofFetchCallback(status); - this.emitter.removeAllListeners(); - }); - this.init(proof); - } - this.signingScheme = SigningScheme.SingleKey; - const pepperBytes = Hex.fromHexInput(pepper).toUint8Array(); - if (pepperBytes.length !== KeylessAccount.PEPPER_LENGTH) { - throw new Error(`Pepper length in bytes should be ${KeylessAccount.PEPPER_LENGTH}`); - } - this.pepper = pepperBytes; - } - - /** - * This initializes the asyncronous proof fetch - * @return - */ - async init(promise: Promise) { - try { - this.proof = await promise; - this.emitter.emit("proofFetchFinish", { status: "Success" }); - } catch (error) { - if (error instanceof Error) { - this.emitter.emit("proofFetchFinish", { status: "Failed", error: error.toString() }); - } else { - this.emitter.emit("proofFetchFinish", { status: "Failed", error: "Unknown" }); - } - } + super(args); } serialize(serializer: Serializer): void { @@ -186,110 +63,6 @@ export class KeylessAccount extends Serializable implements Account { }); } - /** - * Checks if the proof is expired. If so the account must be rederived with a new EphemeralKeyPair - * and JWT token. - * @return boolean - */ - isExpired(): boolean { - return this.ephemeralKeyPair.isExpired(); - } - - /** - * Sign a message using Keyless. - * @param message the message to sign, as binary input - * @return the AccountAuthenticator containing the signature, together with the account's public key - */ - signWithAuthenticator(message: HexInput): AccountAuthenticatorSingleKey { - const signature = new AnySignature(this.sign(message)); - const publicKey = new AnyPublicKey(this.publicKey); - return new AccountAuthenticatorSingleKey(publicKey, signature); - } - - /** - * Sign a transaction using Keyless. - * @param transaction the raw transaction - * @return the AccountAuthenticator containing the signature of the transaction, together with the account's public key - */ - signTransactionWithAuthenticator(transaction: AnyRawTransaction): AccountAuthenticatorSingleKey { - const signature = new AnySignature(this.signTransaction(transaction)); - const publicKey = new AnyPublicKey(this.publicKey); - return new AccountAuthenticatorSingleKey(publicKey, signature); - } - - /** - * Waits for asyncronous proof fetching to finish. - * @return - */ - async waitForProofFetch() { - if (this.proofOrPromise instanceof Promise) { - await this.proofOrPromise; - } - } - - /** - * Sign the given message using Keyless. - * @param message in HexInput format - * @returns Signature - */ - sign(data: HexInput): KeylessSignature { - const { expiryDateSecs } = this.ephemeralKeyPair; - if (this.isExpired()) { - throw new Error("EphemeralKeyPair is expired"); - } - if (this.proof === undefined) { - throw new Error("Proof not defined"); - } - const ephemeralPublicKey = this.ephemeralKeyPair.getPublicKey(); - const ephemeralSignature = this.ephemeralKeyPair.sign(data); - - return new KeylessSignature({ - jwtHeader: base64UrlDecode(this.jwt.split(".")[0]), - ephemeralCertificate: new EphemeralCertificate(this.proof, EphemeralCertificateVariant.ZkProof), - expiryDateSecs, - ephemeralPublicKey, - ephemeralSignature, - }); - } - - /** - * Sign the given transaction with Keyless. - * Signs the transaction and proof to guard against proof malleability. - * @param transaction the transaction to be signed - * @returns KeylessSignature - */ - signTransaction(transaction: AnyRawTransaction): KeylessSignature { - if (this.proof === undefined) { - throw new Error("Proof not found"); - } - const raw = deriveTransactionType(transaction); - const txnAndProof = new TransactionAndProof(raw, this.proof.proof); - const signMess = txnAndProof.hash(); - return this.sign(signMess); - } - - /** - * Note - This function is currently incomplete and should only be used to verify ownership of the KeylessAccount - * - * Verifies a signature given the message. - * - * TODO: Groth16 proof verification - * - * @param args.message the message that was signed. - * @param args.signature the KeylessSignature to verify - * @returns boolean - */ - verifySignature(args: { message: HexInput; signature: KeylessSignature }): boolean { - const { message, signature } = args; - if (this.isExpired()) { - return false; - } - if (!this.ephemeralKeyPair.getPublicKey().verifySignature({ message, signature: signature.ephemeralSignature })) { - return false; - } - return true; - } - static fromBytes(bytes: Uint8Array): KeylessAccount { return KeylessAccount.deserialize(new Deserializer(bytes)); } @@ -326,61 +99,3 @@ export class KeylessAccount extends Serializable implements Account { }); } } - -/** - * A container class to hold a transaction and a proof. It implements CryptoHashable which is used to create - * the signing message for Keyless transactions. We sign over the proof to ensure non-malleability. - */ -class TransactionAndProof extends Serializable { - /** - * The transaction to sign. - */ - transaction: AnyRawTransactionInstance; - - /** - * The zero knowledge proof used in signing the transaction. - */ - proof?: ZkProof; - - /** - * The domain separator prefix used when hashing. - */ - readonly domainSeparator = "APTOS::TransactionAndProof"; - - constructor(transaction: AnyRawTransactionInstance, proof?: ZkProof) { - super(); - this.transaction = transaction; - this.proof = proof; - } - - serialize(serializer: Serializer): void { - serializer.serializeFixedBytes(this.transaction.bcsToBytes()); - serializer.serializeOption(this.proof); - } - - /** - * Hashes the bcs serialized from of the class. This is the typescript corollary to the BCSCryptoHash macro in aptos-core. - * - * @returns Uint8Array - */ - hash(): Uint8Array { - return generateSigningMessage(this.bcsToBytes(), this.domainSeparator); - } -} - -export type ProofFetchSuccess = { - status: "Success"; -}; - -export type ProofFetchFailure = { - status: "Failed"; - error: string; -}; - -export type ProofFetchStatus = ProofFetchSuccess | ProofFetchFailure; - -export type ProofFetchCallback = (status: ProofFetchStatus) => Promise; - -export interface ProofFetchEvents { - proofFetchFinish: (status: ProofFetchStatus) => void; -} diff --git a/src/account/KeylessAccountCommon.ts b/src/account/KeylessAccountCommon.ts new file mode 100644 index 000000000..d91286239 --- /dev/null +++ b/src/account/KeylessAccountCommon.ts @@ -0,0 +1,329 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import EventEmitter from "eventemitter3"; +import { EphemeralCertificateVariant, HexInput, SigningScheme } from "../types"; +import { AccountAddress } from "../core/accountAddress"; +import { + AnyPublicKey, + AnySignature, + KeylessPublicKey, + KeylessSignature, + EphemeralCertificate, + ZeroKnowledgeSig, + ZkProof, +} from "../core/crypto"; + +import { Account } from "./Account"; +import { EphemeralKeyPair } from "./EphemeralKeyPair"; +import { Hex } from "../core/hex"; +import { AccountAuthenticatorSingleKey } from "../transactions/authenticator/account"; +import { Serializable, Serializer } from "../bcs"; +import { deriveTransactionType, generateSigningMessage } from "../transactions/transactionBuilder/signingMessage"; +import { AnyRawTransaction, AnyRawTransactionInstance } from "../transactions/types"; +import { base64UrlDecode } from "../utils/helpers"; +import { FederatedKeylessPublicKey } from "../core/crypto/federatedKeyless"; + +/** + * Account implementation for the Keyless authentication scheme. This abstract class is used for standard Keyless Accounts + * and Federated Keyless Accounts. + */ +export abstract class KeylessAccountCommon extends Serializable implements Account { + static readonly PEPPER_LENGTH: number = 31; + + /** + * The KeylessPublicKey associated with the account + */ + readonly publicKey: KeylessPublicKey | FederatedKeylessPublicKey; + + /** + * The EphemeralKeyPair used to generate sign. + */ + readonly ephemeralKeyPair: EphemeralKeyPair; + + /** + * The claim on the JWT to identify a user. This is typically 'sub' or 'email'. + */ + readonly uidKey: string; + + /** + * The value of the uidKey claim on the JWT. This intended to be a stable user identifier. + */ + readonly uidVal: string; + + /** + * The value of the 'aud' claim on the JWT, also known as client ID. This is the identifier for the dApp's + * OIDC registration with the identity provider. + */ + readonly aud: string; + + /** + * A value contains 31 bytes of entropy that preserves privacy of the account. Typically fetched from a pepper provider. + */ + readonly pepper: Uint8Array; + + /** + * Account address associated with the account + */ + readonly accountAddress: AccountAddress; + + /** + * The zero knowledge signature (if ready) which contains the proof used to validate the EphemeralKeyPair. + */ + proof: ZeroKnowledgeSig | undefined; + + /** + * The proof of the EphemeralKeyPair or a promise that provides the proof. This is used to allow for awaiting on + * fetching the proof. + */ + readonly proofOrPromise: ZeroKnowledgeSig | Promise; + + /** + * Signing scheme used to sign transactions + */ + readonly signingScheme: SigningScheme; + + /** + * The JWT token used to derive the account + */ + readonly jwt: string; + + /** + * An event emitter used to assist in handling asycronous proof fetching. + */ + private readonly emitter: EventEmitter; + + // Use the static constructor 'create' instead. + constructor(args: { + address?: AccountAddress; + ephemeralKeyPair: EphemeralKeyPair; + iss: string; + uidKey: string; + uidVal: string; + aud: string; + pepper: HexInput; + proof: ZeroKnowledgeSig | Promise; + proofFetchCallback?: ProofFetchCallback; + jwt: string; + }) { + super(); + const { address, ephemeralKeyPair, uidKey, uidVal, aud, pepper, proof, proofFetchCallback, jwt } = args; + this.ephemeralKeyPair = ephemeralKeyPair; + this.publicKey = KeylessPublicKey.create(args); + this.accountAddress = address ? AccountAddress.from(address) : this.publicKey.authKey().derivedAddress(); + this.uidKey = uidKey; + this.uidVal = uidVal; + this.aud = aud; + this.jwt = jwt; + this.emitter = new EventEmitter(); + this.proofOrPromise = proof; + if (proof instanceof ZeroKnowledgeSig) { + this.proof = proof; + } else { + if (proofFetchCallback === undefined) { + throw new Error("Must provide callback for async proof fetch"); + } + this.emitter.on("proofFetchFinish", async (status) => { + await proofFetchCallback(status); + this.emitter.removeAllListeners(); + }); + this.init(proof); + } + this.signingScheme = SigningScheme.SingleKey; + const pepperBytes = Hex.fromHexInput(pepper).toUint8Array(); + if (pepperBytes.length !== KeylessAccountCommon.PEPPER_LENGTH) { + throw new Error(`Pepper length in bytes should be ${KeylessAccountCommon.PEPPER_LENGTH}`); + } + this.pepper = pepperBytes; + } + + /** + * This initializes the asyncronous proof fetch + * @return + */ + async init(promise: Promise) { + try { + this.proof = await promise; + this.emitter.emit("proofFetchFinish", { status: "Success" }); + } catch (error) { + if (error instanceof Error) { + this.emitter.emit("proofFetchFinish", { status: "Failed", error: error.toString() }); + } else { + this.emitter.emit("proofFetchFinish", { status: "Failed", error: "Unknown" }); + } + } + } + + serialize(serializer: Serializer): void { + serializer.serializeStr(this.jwt); + serializer.serializeStr(this.uidKey); + serializer.serializeFixedBytes(this.pepper); + this.ephemeralKeyPair.serialize(serializer); + if (this.proof === undefined) { + throw new Error("Connot serialize - proof undefined"); + } + this.proof.serialize(serializer); + } + + /** + * Checks if the proof is expired. If so the account must be rederived with a new EphemeralKeyPair + * and JWT token. + * @return boolean + */ + isExpired(): boolean { + return this.ephemeralKeyPair.isExpired(); + } + + /** + * Sign a message using Keyless. + * @param message the message to sign, as binary input + * @return the AccountAuthenticator containing the signature, together with the account's public key + */ + signWithAuthenticator(message: HexInput): AccountAuthenticatorSingleKey { + const signature = new AnySignature(this.sign(message)); + const publicKey = new AnyPublicKey(this.publicKey); + return new AccountAuthenticatorSingleKey(publicKey, signature); + } + + /** + * Sign a transaction using Keyless. + * @param transaction the raw transaction + * @return the AccountAuthenticator containing the signature of the transaction, together with the account's public key + */ + signTransactionWithAuthenticator(transaction: AnyRawTransaction): AccountAuthenticatorSingleKey { + const signature = new AnySignature(this.signTransaction(transaction)); + const publicKey = new AnyPublicKey(this.publicKey); + return new AccountAuthenticatorSingleKey(publicKey, signature); + } + + /** + * Waits for asyncronous proof fetching to finish. + * @return + */ + async waitForProofFetch() { + if (this.proofOrPromise instanceof Promise) { + await this.proofOrPromise; + } + } + + /** + * Sign the given message using Keyless. + * @param message in HexInput format + * @returns Signature + */ + sign(data: HexInput): KeylessSignature { + const { expiryDateSecs } = this.ephemeralKeyPair; + if (this.isExpired()) { + throw new Error("EphemeralKeyPair is expired"); + } + if (this.proof === undefined) { + throw new Error("Proof not defined"); + } + const ephemeralPublicKey = this.ephemeralKeyPair.getPublicKey(); + const ephemeralSignature = this.ephemeralKeyPair.sign(data); + + return new KeylessSignature({ + jwtHeader: base64UrlDecode(this.jwt.split(".")[0]), + ephemeralCertificate: new EphemeralCertificate(this.proof, EphemeralCertificateVariant.ZkProof), + expiryDateSecs, + ephemeralPublicKey, + ephemeralSignature, + }); + } + + /** + * Sign the given transaction with Keyless. + * Signs the transaction and proof to guard against proof malleability. + * @param transaction the transaction to be signed + * @returns KeylessSignature + */ + signTransaction(transaction: AnyRawTransaction): KeylessSignature { + if (this.proof === undefined) { + throw new Error("Proof not found"); + } + const raw = deriveTransactionType(transaction); + const txnAndProof = new TransactionAndProof(raw, this.proof.proof); + const signMess = txnAndProof.hash(); + return this.sign(signMess); + } + + /** + * Note - This function is currently incomplete and should only be used to verify ownership of the KeylessAccount + * + * Verifies a signature given the message. + * + * TODO: Groth16 proof verification + * + * @param args.message the message that was signed. + * @param args.signature the KeylessSignature to verify + * @returns boolean + */ + verifySignature(args: { message: HexInput; signature: KeylessSignature }): boolean { + const { message, signature } = args; + if (this.isExpired()) { + return false; + } + if (!this.ephemeralKeyPair.getPublicKey().verifySignature({ message, signature: signature.ephemeralSignature })) { + return false; + } + return true; + } +} + +/** + * A container class to hold a transaction and a proof. It implements CryptoHashable which is used to create + * the signing message for Keyless transactions. We sign over the proof to ensure non-malleability. + */ +export class TransactionAndProof extends Serializable { + /** + * The transaction to sign. + */ + transaction: AnyRawTransactionInstance; + + /** + * The zero knowledge proof used in signing the transaction. + */ + proof?: ZkProof; + + /** + * The domain separator prefix used when hashing. + */ + readonly domainSeparator = "APTOS::TransactionAndProof"; + + constructor(transaction: AnyRawTransactionInstance, proof?: ZkProof) { + super(); + this.transaction = transaction; + this.proof = proof; + } + + serialize(serializer: Serializer): void { + serializer.serializeFixedBytes(this.transaction.bcsToBytes()); + serializer.serializeOption(this.proof); + } + + /** + * Hashes the bcs serialized from of the class. This is the typescript corollary to the BCSCryptoHash macro in aptos-core. + * + * @returns Uint8Array + */ + hash(): Uint8Array { + return generateSigningMessage(this.bcsToBytes(), this.domainSeparator); + } +} + +export type ProofFetchSuccess = { + status: "Success"; +}; + +export type ProofFetchFailure = { + status: "Failed"; + error: string; +}; + +export type ProofFetchStatus = ProofFetchSuccess | ProofFetchFailure; + +export type ProofFetchCallback = (status: ProofFetchStatus) => Promise; + +export interface ProofFetchEvents { + proofFetchFinish: (status: ProofFetchStatus) => void; +} diff --git a/src/account/index.ts b/src/account/index.ts index 53d6d7ccd..b246b7b1d 100644 --- a/src/account/index.ts +++ b/src/account/index.ts @@ -3,4 +3,6 @@ export * from "./Account"; export * from "./SingleKeyAccount"; export * from "./EphemeralKeyPair"; export * from "./KeylessAccount"; +export * from "./KeylessAccountCommon"; +export * from "./FederatedKeylessAccount"; export * from "./MultiKeyAccount"; diff --git a/src/api/keyless.ts b/src/api/keyless.ts index 3df7e45fb..3ee123582 100644 --- a/src/api/keyless.ts +++ b/src/api/keyless.ts @@ -1,9 +1,16 @@ // Copyright © Aptos Foundation // SPDX-License-Identifier: Apache-2.0 -import { EphemeralKeyPair, KeylessAccount, ProofFetchCallback } from "../account"; -import { ZeroKnowledgeSig } from "../core"; -import { deriveKeylessAccount, getPepper, getProof } from "../internal/keyless"; +import { Account, EphemeralKeyPair, KeylessAccount, ProofFetchCallback } from "../account"; +import { FederatedKeylessAccount } from "../account/FederatedKeylessAccount"; +import { AccountAddressInput, ZeroKnowledgeSig } from "../core"; +import { + deriveKeylessAccount, + getPepper, + getProof, + updateFederatedKeylessJwkSetTransaction, +} from "../internal/keyless"; +import { SimpleTransaction } from "../transactions"; import { HexInput } from "../types"; import { AptosConfig } from "./aptosConfig"; @@ -52,12 +59,30 @@ export class Keyless { return getProof({ aptosConfig: this.config, ...args }); } + async deriveKeylessAccount(args: { + jwt: string; + ephemeralKeyPair: EphemeralKeyPair; + uidKey?: string; + pepper?: HexInput; + proofFetchCallback?: ProofFetchCallback; + }): Promise; + + async deriveKeylessAccount(args: { + jwt: string; + ephemeralKeyPair: EphemeralKeyPair; + jwkAddress: AccountAddressInput; + uidKey?: string; + pepper?: HexInput; + proofFetchCallback?: ProofFetchCallback; + }): Promise; + /** * Derives the Keyless Account from the JWT token and corresponding EphemeralKeyPair. It will lookup the pepper from * the pepper service if not explicitly provided. It will compute the proof via the proving service. It will ch * * @param args.jwt JWT token * @param args.ephemeralKeyPair the EphemeralKeyPair used to generate the nonce in the JWT token + * @param args.jwkAddress the where the JWKs used to verify signatures are found. Setting the value derives a FederatedKeylessAccount * @param args.uidKey a key in the JWT token to use to set the uidVal in the IdCommitment * @param args.pepper the pepper * @param args.proofFetchCallback a callback function that if set, the fetch of the proof will be done in the background. Once @@ -69,10 +94,31 @@ export class Keyless { async deriveKeylessAccount(args: { jwt: string; ephemeralKeyPair: EphemeralKeyPair; + jwkAddress?: AccountAddressInput; uidKey?: string; pepper?: HexInput; proofFetchCallback?: ProofFetchCallback; - }): Promise { + }): Promise { return deriveKeylessAccount({ aptosConfig: this.config, ...args }); } + + /** + * This installs a set of FederatedJWKs at an address for a given iss. + * + * It will fetch the JWK set from the well-known endpoint and update the FederatedJWKs at the sender's address + * to reflect it. + * + * @param args.sender The account that will install the JWKs + * @param args.iss the iss claim of the federated OIDC provider. + * @param args.jwksUrl the URL to find the corresponding JWKs. For supported IDP providers this parameter in not necessary. + * + * @returns The pending transaction that results from submission. + */ + async updateFederatedKeylessJwkSetTransaction(args: { + sender: Account; + iss: string; + jwksUrl?: string; + }): Promise { + return updateFederatedKeylessJwkSetTransaction({ aptosConfig: this.config, ...args }); + } } diff --git a/src/core/crypto/federatedKeyless.ts b/src/core/crypto/federatedKeyless.ts new file mode 100644 index 000000000..2cfc26829 --- /dev/null +++ b/src/core/crypto/federatedKeyless.ts @@ -0,0 +1,134 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { AccountPublicKey, PublicKey } from "./publicKey"; +import { Deserializer, Serializer } from "../../bcs"; +import { Hex } from "../hex"; +import { HexInput, AnyPublicKeyVariant, SigningScheme } from "../../types"; +import { AuthenticationKey } from "../authenticationKey"; +import { AccountAddress, AccountAddressInput } from "../accountAddress"; +import { KeylessPublicKey, KeylessSignature } from "./keyless"; + +/** + * Represents the FederatedKeylessPublicKey public key + * + * These keys use an onchain address as a source of truth for the JWK used to verify signatures. + * + * FederatedKeylessPublicKey authentication key is represented in the SDK as `AnyPublicKey`. + */ +export class FederatedKeylessPublicKey extends AccountPublicKey { + /** + * The address that contains the JWK set to be used for verification. + */ + readonly jwkAddress: AccountAddress; + + /** + * The inner public key which contains the standard Keyless public key. + */ + readonly keylessPublicKey: KeylessPublicKey; + + constructor(jwkAddress: AccountAddressInput, keylessPublicKey: KeylessPublicKey) { + super(); + this.jwkAddress = AccountAddress.from(jwkAddress); + this.keylessPublicKey = keylessPublicKey; + } + + /** + * Get the authentication key for the federated keyless public key + * + * @returns AuthenticationKey + */ + authKey(): AuthenticationKey { + const serializer = new Serializer(); + serializer.serializeU32AsUleb128(AnyPublicKeyVariant.FederatedKeyless); + serializer.serializeFixedBytes(this.bcsToBytes()); + return AuthenticationKey.fromSchemeAndBytes({ + scheme: SigningScheme.SingleKey, + input: serializer.toUint8Array(), + }); + } + + /** + * Get the public key in bytes (Uint8Array). + * + * @returns Uint8Array representation of the public key + */ + toUint8Array(): Uint8Array { + return this.bcsToBytes(); + } + + /** + * Get the public key as a hex string with the 0x prefix. + * + * @returns string representation of the public key + */ + toString(): string { + return Hex.fromHexInput(this.toUint8Array()).toString(); + } + + /** + * Verifies a signed data with a public key + * + * @param args.message message + * @param args.signature The signature + * @returns true if the signature is valid + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this + verifySignature(args: { message: HexInput; signature: KeylessSignature }): boolean { + throw new Error("Not yet implemented"); + } + + serialize(serializer: Serializer): void { + this.jwkAddress.serialize(serializer); + this.keylessPublicKey.serialize(serializer); + } + + static deserialize(deserializer: Deserializer): FederatedKeylessPublicKey { + const jwkAddress = AccountAddress.deserialize(deserializer); + const keylessPublicKey = KeylessPublicKey.deserialize(deserializer); + return new FederatedKeylessPublicKey(jwkAddress, keylessPublicKey); + } + + static isPublicKey(publicKey: PublicKey): publicKey is FederatedKeylessPublicKey { + return publicKey instanceof FederatedKeylessPublicKey; + } + + /** + * Creates a FederatedKeylessPublicKey from the JWT components plus pepper + * + * @param args.iss the iss of the identity + * @param args.uidKey the key to use to get the uidVal in the JWT token + * @param args.uidVal the value of the uidKey in the JWT token + * @param args.aud the client ID of the application + * @param args.pepper The pepper used to maintain privacy of the account + * @returns FederatedKeylessPublicKey + */ + static create(args: { + iss: string; + uidKey: string; + uidVal: string; + aud: string; + pepper: HexInput; + jwkAddress: AccountAddressInput; + }): FederatedKeylessPublicKey { + return new FederatedKeylessPublicKey(args.jwkAddress, KeylessPublicKey.create(args)); + } + + static fromJwtAndPepper(args: { + jwt: string; + pepper: HexInput; + jwkAddress: AccountAddressInput; + uidKey?: string; + }): FederatedKeylessPublicKey { + return new FederatedKeylessPublicKey(args.jwkAddress, KeylessPublicKey.fromJwtAndPepper(args)); + } + + static isInstance(publicKey: PublicKey) { + return ( + "jwkAddress" in publicKey && + publicKey.jwkAddress instanceof AccountAddress && + "keylessPublicKey" in publicKey && + publicKey.keylessPublicKey instanceof KeylessPublicKey + ); + } +} diff --git a/src/core/crypto/index.ts b/src/core/crypto/index.ts index 6b3c87573..c5227b4ed 100644 --- a/src/core/crypto/index.ts +++ b/src/core/crypto/index.ts @@ -6,6 +6,7 @@ export * from "./hdKey"; export * from "./multiEd25519"; export * from "./multiKey"; export * from "./ephemeral"; +export * from "./federatedKeyless"; export * from "./keyless"; export * from "./privateKey"; export * from "./publicKey"; diff --git a/src/core/crypto/singleKey.ts b/src/core/crypto/singleKey.ts index 72f34a761..16ecaf1fe 100644 --- a/src/core/crypto/singleKey.ts +++ b/src/core/crypto/singleKey.ts @@ -6,6 +6,7 @@ import { AccountPublicKey, PublicKey, VerifySignatureArgs } from "./publicKey"; import { Secp256k1PublicKey, Secp256k1Signature } from "./secp256k1"; import { KeylessPublicKey, KeylessSignature } from "./keyless"; import { Signature } from "./signature"; +import { FederatedKeylessPublicKey } from "./federatedKeyless"; /** * Represents any public key supported by Aptos. @@ -37,6 +38,8 @@ export class AnyPublicKey extends AccountPublicKey { this.variant = AnyPublicKeyVariant.Secp256k1; } else if (publicKey instanceof KeylessPublicKey) { this.variant = AnyPublicKeyVariant.Keyless; + } else if (publicKey instanceof FederatedKeylessPublicKey) { + this.variant = AnyPublicKeyVariant.FederatedKeyless; } else { throw new Error("Unsupported public key type"); } @@ -91,6 +94,9 @@ export class AnyPublicKey extends AccountPublicKey { case AnyPublicKeyVariant.Keyless: publicKey = KeylessPublicKey.deserialize(deserializer); break; + case AnyPublicKeyVariant.FederatedKeyless: + publicKey = FederatedKeylessPublicKey.deserialize(deserializer); + break; default: throw new Error(`Unknown variant index for AnyPublicKey: ${variantIndex}`); } diff --git a/src/internal/keyless.ts b/src/internal/keyless.ts index 66da3872e..4aea51bda 100644 --- a/src/internal/keyless.ts +++ b/src/internal/keyless.ts @@ -7,9 +7,11 @@ * other namespaces and processes can access these methods without depending on the entire * keyless namespace and without having a dependency cycle error. */ +import { jwtDecode, JwtPayload } from "jwt-decode"; import { AptosConfig } from "../api/aptosConfig"; import { postAptosPepperService, postAptosProvingService } from "../client"; import { + AccountAddressInput, EphemeralSignature, Groth16Zkp, Hex, @@ -19,10 +21,14 @@ import { getKeylessConfig, } from "../core"; import { HexInput, ZkpVariant } from "../types"; -import { EphemeralKeyPair, KeylessAccount, ProofFetchCallback } from "../account"; +import { Account, EphemeralKeyPair, KeylessAccount, ProofFetchCallback } from "../account"; import { PepperFetchRequest, PepperFetchResponse, ProverRequest, ProverResponse } from "../types/keyless"; -import { nowInSeconds } from "../utils/helpers"; import { lookupOriginalAccountAddress } from "./account"; +import { FederatedKeylessPublicKey } from "../core/crypto/federatedKeyless"; +import { FederatedKeylessAccount } from "../account/FederatedKeylessAccount"; +import { MoveVector } from "../bcs"; +import { generateTransaction } from "./transactionSubmission"; +import { SimpleTransaction } from "../transactions"; export async function getPepper(args: { aptosConfig: AptosConfig; @@ -63,7 +69,7 @@ export async function getProof(args: { throw new Error(`Pepper needs to be ${KeylessAccount.PEPPER_LENGTH} bytes`); } const { maxExpHorizonSecs } = await getKeylessConfig({ aptosConfig }); - if (maxExpHorizonSecs < ephemeralKeyPair.expiryDateSecs - nowInSeconds()) { + if (maxExpHorizonSecs < ephemeralKeyPair.expiryDateSecs - jwtDecode(jwt).iat!) { throw Error(`The EphemeralKeyPair is too long lived. It's lifespan must be less than ${maxExpHorizonSecs}`); } const json = { @@ -106,8 +112,28 @@ export async function deriveKeylessAccount(args: { uidKey?: string; pepper?: HexInput; proofFetchCallback?: ProofFetchCallback; -}): Promise { - const { aptosConfig, jwt, uidKey, proofFetchCallback, pepper = await getPepper(args) } = args; +}): Promise; + +export async function deriveKeylessAccount(args: { + aptosConfig: AptosConfig; + jwt: string; + ephemeralKeyPair: EphemeralKeyPair; + jwkAddress: AccountAddressInput; + uidKey?: string; + pepper?: HexInput; + proofFetchCallback?: ProofFetchCallback; +}): Promise; + +export async function deriveKeylessAccount(args: { + aptosConfig: AptosConfig; + jwt: string; + ephemeralKeyPair: EphemeralKeyPair; + jwkAddress?: AccountAddressInput; + uidKey?: string; + pepper?: HexInput; + proofFetchCallback?: ProofFetchCallback; +}): Promise { + const { aptosConfig, jwt, jwkAddress, uidKey, proofFetchCallback, pepper = await getPepper(args) } = args; const proofPromise = getProof({ ...args, pepper }); // If a callback is provided, pass in the proof as a promise to KeylessAccount.create. This will make the proof be fetched in the // background and the callback will handle the outcome of the fetch. This allows the developer to not have to block on the proof fetch @@ -116,14 +142,62 @@ export async function deriveKeylessAccount(args: { // If no callback is provided, the just await the proof fetch and continue syncronously. const proof = proofFetchCallback ? proofPromise : await proofPromise; - // Look up the original address to handle key rotations + // Look up the original address to handle key rotations and then instantiate the account. + if (jwkAddress !== undefined) { + const publicKey = FederatedKeylessPublicKey.fromJwtAndPepper({ jwt, pepper, jwkAddress, uidKey }); + const address = await lookupOriginalAccountAddress({ + aptosConfig, + authenticationKey: publicKey.authKey().derivedAddress(), + }); + + return FederatedKeylessAccount.create({ ...args, address, proof, pepper, proofFetchCallback, jwkAddress }); + } + const publicKey = KeylessPublicKey.fromJwtAndPepper({ jwt, pepper, uidKey }); const address = await lookupOriginalAccountAddress({ aptosConfig, authenticationKey: publicKey.authKey().derivedAddress(), }); + return KeylessAccount.create({ ...args, address, proof, pepper, proofFetchCallback }); +} - const keylessAccount = KeylessAccount.create({ ...args, address, proof, pepper, proofFetchCallback }); +interface JWK { + kty: string; // Key type + kid: string; // Key ID + alg: string; // Algorithm used with the key + n: string; // Modulus (for RSA keys) + e: string; // Exponent (for RSA keys) +} - return keylessAccount; +interface JWKS { + keys: JWK[]; +} + +export async function updateFederatedKeylessJwkSetTransaction(args: { + aptosConfig: AptosConfig; + sender: Account; + iss: string; + jwksUrl?: string; +}): Promise { + const { aptosConfig, sender, iss } = args; + const jwksUrl = args.jwksUrl ?? (iss.endsWith("/") ? `${iss}.well-known/jwks.json` : `${iss}/.well-known/jwks.json`); + const response = await fetch(jwksUrl); + if (!response.ok) { + throw new Error(`Failed to fetch JWKS: ${response.status} ${response.statusText}`); + } + const jwks: JWKS = await response.json(); + return generateTransaction({ + aptosConfig, + sender: sender.accountAddress, + data: { + function: "0x1::jwks::update_federated_jwk_set", + functionArguments: [ + iss, + MoveVector.MoveString(jwks.keys.map((key) => key.kid)), + MoveVector.MoveString(jwks.keys.map((key) => key.alg)), + MoveVector.MoveString(jwks.keys.map((key) => key.e)), + MoveVector.MoveString(jwks.keys.map((key) => key.n)), + ], + }, + }); } diff --git a/src/internal/transactionSubmission.ts b/src/internal/transactionSubmission.ts index 4d149e27f..bbc1e57eb 100644 --- a/src/internal/transactionSubmission.ts +++ b/src/internal/transactionSubmission.ts @@ -8,7 +8,7 @@ import { AptosConfig } from "../api/aptosConfig"; import { MoveVector, U8 } from "../bcs"; import { postAptosFullNode } from "../client"; -import { Account, KeylessAccount, MultiKeyAccount } from "../account"; +import { Account, KeylessAccountCommon, MultiKeyAccount } from "../account"; import { AccountAddress, AccountAddressInput } from "../core/accountAddress"; import { PrivateKey } from "../core/crypto"; import { AccountAuthenticator } from "../transactions/authenticator/account"; @@ -277,7 +277,7 @@ export async function signAndSubmitTransaction(args: { const { aptosConfig, signer, transaction } = args; // If the signer contains a KeylessAccount, await proof fetching in case the proof // was fetched asyncronously. - if (signer instanceof KeylessAccount || signer instanceof MultiKeyAccount) { + if (signer instanceof KeylessAccountCommon || signer instanceof MultiKeyAccount) { await signer.waitForProofFetch(); } const authenticator = signTransaction({ signer, transaction }); diff --git a/src/transactions/transactionBuilder/transactionBuilder.ts b/src/transactions/transactionBuilder/transactionBuilder.ts index e777a100e..9adcdabb8 100644 --- a/src/transactions/transactionBuilder/transactionBuilder.ts +++ b/src/transactions/transactionBuilder/transactionBuilder.ts @@ -9,7 +9,14 @@ import { sha3_256 as sha3Hash } from "@noble/hashes/sha3"; import { AptosConfig } from "../../api/aptosConfig"; import { AccountAddress, AccountAddressInput, Hex, PublicKey } from "../../core"; -import { AnyPublicKey, AnySignature, KeylessPublicKey, KeylessSignature, Secp256k1PublicKey } from "../../core/crypto"; +import { + AnyPublicKey, + AnySignature, + KeylessPublicKey, + KeylessSignature, + Secp256k1PublicKey, + FederatedKeylessPublicKey, +} from "../../core/crypto"; import { Ed25519PublicKey, Ed25519Signature } from "../../core/crypto/ed25519"; import { getInfo } from "../../internal/account"; import { getLedgerInfo } from "../../internal/general"; @@ -445,25 +452,29 @@ export function generateSignedTransactionForSimulation(args: InputSimulateTransa } export function getAuthenticatorForSimulation(publicKey: PublicKey) { + // Wrap the public key types below with AnyPublicKey as they are only support through single sender. + // Learn more about AnyPublicKey here - https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-55.md + const convertToAnyPublicKey = + KeylessPublicKey.isInstance(publicKey) || + FederatedKeylessPublicKey.isInstance(publicKey) || + Secp256k1PublicKey.isInstance(publicKey); + const accountPublicKey = convertToAnyPublicKey ? new AnyPublicKey(publicKey) : publicKey; + // No need to for the signature to be matching in scheme. All that matters for simulations is that it's not valid const invalidSignature = new Ed25519Signature(new Uint8Array(64)); - if (Ed25519PublicKey.isInstance(publicKey)) { - return new AccountAuthenticatorEd25519(publicKey, invalidSignature); + if (Ed25519PublicKey.isInstance(accountPublicKey)) { + return new AccountAuthenticatorEd25519(accountPublicKey, invalidSignature); } - if (AnyPublicKey.isInstance(publicKey)) { - if (KeylessPublicKey.isInstance(publicKey.publicKey)) { - return new AccountAuthenticatorSingleKey(publicKey, new AnySignature(KeylessSignature.getSimulationSignature())); + if (AnyPublicKey.isInstance(accountPublicKey)) { + if (KeylessPublicKey.isInstance(accountPublicKey.publicKey)) { + return new AccountAuthenticatorSingleKey( + accountPublicKey, + new AnySignature(KeylessSignature.getSimulationSignature()), + ); } - return new AccountAuthenticatorSingleKey(publicKey, new AnySignature(invalidSignature)); - } - - // TODO: remove this, non-account public keys should never make it here - if (KeylessPublicKey.isInstance(publicKey) || Secp256k1PublicKey.isInstance(publicKey)) { - // eslint-disable-next-line no-console - console.warn("Expected AccountPublicKey, but got PublicKey. Please wrap your public key with AnyPublicKey."); - return new AccountAuthenticatorSingleKey(new AnyPublicKey(publicKey), new AnySignature(invalidSignature)); + return new AccountAuthenticatorSingleKey(accountPublicKey, new AnySignature(invalidSignature)); } // TODO add support for AnyMultiKey diff --git a/src/types/index.ts b/src/types/index.ts index 7c250a296..15a6dc208 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -110,6 +110,7 @@ export enum AnyPublicKeyVariant { Ed25519 = 0, Secp256k1 = 1, Keyless = 3, + FederatedKeyless = 4, } export enum AnySignatureVariant { diff --git a/tests/e2e/api/keyless.test.ts b/tests/e2e/api/keyless.test.ts index 44ea57c98..6ab02e75c 100644 --- a/tests/e2e/api/keyless.test.ts +++ b/tests/e2e/api/keyless.test.ts @@ -2,7 +2,14 @@ // Copyright © Aptos Foundation // SPDX-License-Identifier: Apache-2.0 -import { Account, KeylessAccount, KeylessPublicKey, ProofFetchStatus } from "../../../src"; +import { + Account, + FederatedKeylessAccount, + FederatedKeylessPublicKey, + KeylessAccount, + KeylessPublicKey, + ProofFetchStatus, +} from "../../../src"; import { FUND_AMOUNT, TRANSFER_AMOUNT } from "../../unit/helper"; import { getAptosClient } from "../helper"; import { EPHEMERAL_KEY_PAIR, simpleCoinTransactionHeler as simpleCoinTransactionHelper } from "../transaction/helper"; @@ -30,22 +37,88 @@ export const TEST_JWT_TOKENS = [ "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0Lm9pZGMucHJvdmlkZXIiLCJhdWQiOiJ0ZXN0LWtleWxlc3MtZGFwcCIsInN1YiI6InRlc3QtdXNlci0xOSIsImVtYWlsIjoidGVzdEBhcHRvc2xhYnMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlhdCI6MTcyNTQ3NTExMiwiZXhwIjoyNzAwMDAwMDAwLCJub25jZSI6IjcwOTUyNDIzMzM5NjQ0NTcyNjc5MzQ3MjM3NjgwODAzMDMzMjQ0NjI4MjExOTE3NTY0MDk0NTAwOTk1MTk3ODEwNTE5MTAxODcxMTgifQ.elbm8TQ1qHbxKmTPq0ShHiRNiqZ_bF_GG25Gjeb4JliYz4PTrxtocXX4Frez_4nf7mCbgcB37fkuJrcyHQ5QaCxjWrVzqUzgMWnRt2ryMnj4tN9Oz4O3Yidoqkxz726iJ7X1FnRgNaG4OGLUItLfYDKTBbuSKzdHyGA5zsBEKVmz0FKL9HdD66D44alUddg1MUAbphxBG4ghh0mZg8DjsXCCsxO547xvgScK-tGt3_I8wEyS-D_-bEElaLPnW87wFoLosHIZucf38PdadWxk6gSt3MRdErLHeP42DwsVwv7vF2b1aTek5au-f-FUXTkidyEfKKuHtmIS3ZKltyJ1Pw", ]; +export const TEST_FEDERATED_JWT_TOKENS = [ + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItMCIsImVtYWlsIjoidGVzdEBhcHRvc2xhYnMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlhdCI6MTcyNTQ3NTExMiwiZXhwIjoyNzAwMDAwMDAwLCJub25jZSI6IjcwOTUyNDIzMzM5NjQ0NTcyNjc5MzQ3MjM3NjgwODAzMDMzMjQ0NjI4MjExOTE3NTY0MDk0NTAwOTk1MTk3ODEwNTE5MTAxODcxMTgifQ.fZPN9kj5H1xf7YS81Rll_3-BC1yOIngoAmr4rFdvMtLqRxNPfLncyUFjsTfsf5USh-LwjQli8fb6uDSmnueGCbQQyw5CDSaALFmSMqzzr_ZezQv8Bv7iXrLPKei9Xp0eogoYsbb3_2PEu8DxwjXAD285uj5QPx0cTaoo--yf80rvvQYbHJp62XEd82mpM0vvaZqF6T_33STfj06Yj8B0RhAfKTSCTCa_7njriykbxIN7O5b_xPnhkRSJIjqD0ZmvNex4MuxMaHgd6Zgs9OIymzYh5dgsrC4Z0OawXCjK56N7SdaXwE6iFRF1g6Yy0pFxhMsX6_KtZR3-xzDndUvxwg", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItMSIsImVtYWlsIjoidGVzdEBhcHRvc2xhYnMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlhdCI6MTcyNTQ3NTExMiwiZXhwIjoyNzAwMDAwMDAwLCJub25jZSI6IjcwOTUyNDIzMzM5NjQ0NTcyNjc5MzQ3MjM3NjgwODAzMDMzMjQ0NjI4MjExOTE3NTY0MDk0NTAwOTk1MTk3ODEwNTE5MTAxODcxMTgifQ.N7s0TZyE6bRjxWPGIlpjVb9c0jI_5dWgJsdWyYj6WQ1FEK4zb4b6nmCjHAJ-3dVrU9vJkOZruWqnpk-c7KcbS4Ouq4TzQj6FMbE4DBDvG0GF_TDvw5-Wbs_KU7dWcStqvDaq1ugzs2ZVnYlIp_p-VfK1kTY-N-4-nk2Xmv62mStF5ShORBUOraKmrWuwC6gnYC6-srZbvufdN-9GOXqMNiJMHonrEM66qDjsSCl44_lmj91ze5GzLKv0ggCNc-ZVm5Tj4fmre6Ba6RUxNi9xUHrzNoEZHg-Y1ZlNXgfozw3pkSrA5CDp5xonjVFfHQR_J_sXHKozk_9K3dNkOeBA5Q", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItMiIsImVtYWlsIjoidGVzdEBhcHRvc2xhYnMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlhdCI6MTcyNTQ3NTExMiwiZXhwIjoyNzAwMDAwMDAwLCJub25jZSI6IjcwOTUyNDIzMzM5NjQ0NTcyNjc5MzQ3MjM3NjgwODAzMDMzMjQ0NjI4MjExOTE3NTY0MDk0NTAwOTk1MTk3ODEwNTE5MTAxODcxMTgifQ.fLiHkTWpSR-ux61nlRLBIGE8XDYP23DE1h9VJpb3CXX-LYj3A9B7YvvLRXUZ4Ffl3DLV97GsT_tHEEB71VHiJ56xpwxUWxkHdgWgxFLaWgtuW4kBvydIvyMOWbYIoeO2Np6Ef1q58VjY5xs8RAMSFHhY6VDJ0vTZn1hgw01Qf041boIFNjIYvxSvLtA0-B99uq0mI2TOquLBjG-N57mnukW6oLSWUNOC1ysmrWavYIklTJxTShOpufNzB-oH3zD7O_INSWnboyIabGz8TC2njMd4mub4KwlyZJZ00zyHs5sMLG1W6M0V8RHrPIas1G4ic0drvHRlkiLLDlCcEZkYbg", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItMyIsImVtYWlsIjoidGVzdEBhcHRvc2xhYnMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlhdCI6MTcyNTQ3NTExMiwiZXhwIjoyNzAwMDAwMDAwLCJub25jZSI6IjcwOTUyNDIzMzM5NjQ0NTcyNjc5MzQ3MjM3NjgwODAzMDMzMjQ0NjI4MjExOTE3NTY0MDk0NTAwOTk1MTk3ODEwNTE5MTAxODcxMTgifQ.Ccm2d37bJ1GGaodtzztPWlmp_Aa8XdrvtIpB84JPjJKlijIwvRiENC9p_9EwinpTEAZqyJHDniHw1mn29p1tXjoA1rGwj0YmcbtS3oYdXgiva8leuPzAUfRmwIxateIjIoY6ggYVctPLa_yIomVPhB8TBS2sGq0kTA_VAiQ2MnU8RuPJyXhl_bE27DbVLzTaESDIN56SO7Hwz5EwsSNzt8hiFKScppRpzUEEQ2EIIUkGePpnVxNtGAmi7DXVvd17AoYADnjQZCelpQcBE6_WUMc1_9MHUFVTJwTdf3lPYIlGAXCqmHNvaz1sUf7gzrkZCub2BYzhq8zg458EvG8E6g", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItNCIsImVtYWlsIjoidGVzdEBhcHRvc2xhYnMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlhdCI6MTcyNTQ3NTExMiwiZXhwIjoyNzAwMDAwMDAwLCJub25jZSI6IjcwOTUyNDIzMzM5NjQ0NTcyNjc5MzQ3MjM3NjgwODAzMDMzMjQ0NjI4MjExOTE3NTY0MDk0NTAwOTk1MTk3ODEwNTE5MTAxODcxMTgifQ.aMFGHKNeIHkBE8woc-jiTFAB1sm7RE0sCxQcNKkiaak7zCXbUrdKtImhht9yx_VmJ8e2FubH8eVh92P8tlIGaTpwdtu5qq8i71l7nX3Qk2DFxUAbAkG92suTnzCQE91D5Rtt8dlvmwHytx-a7Dr0Gv4M1JPon1y5vSkBrSsLIqiK6CA9othikadZRVUwO0ubckNldlhPjE7wz59Qf9Q3UNFFcIFVaGM5BsLIpMiZZOwEuNqitD243kPXR3gBBmPwYygMV_HtpSA8IXl95Aojg4KftZFJG7cZkLd-g_tz5KW-4YptrB7-3URK4K4IwNQJ9XUHbzDrMcYaY534YCVoVw", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItNSIsImVtYWlsIjoidGVzdEBhcHRvc2xhYnMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlhdCI6MTcyNTQ3NTExMiwiZXhwIjoyNzAwMDAwMDAwLCJub25jZSI6IjcwOTUyNDIzMzM5NjQ0NTcyNjc5MzQ3MjM3NjgwODAzMDMzMjQ0NjI4MjExOTE3NTY0MDk0NTAwOTk1MTk3ODEwNTE5MTAxODcxMTgifQ.HZFhdvodQLF3fGH9u6beqWsfugFS1OHkZMMhkTWRVhbzREs5g91_GzJcchXetotbhBmcCuQ_apLFFwOWHyx0VFTm2WvJdd1uUYrn4sKllFok9709ZLOfBlbo4218C-xA8LEIBoz6AxXfm8CVIy3FFjUGaKkniWjCdofWa6bSkxCXqMrGk84IaQP96S_d52ooG_B3H-Gb9mbEYRgBP3zEgVEq0bqMiJQJ-CNPBIeKAkUp189kOS3EHhheggNPbR8g3ee0AtNXlHbDDWdIQFb223ufFuX6igA5aWoIUFIBZ-URx0oy099ua8Pem5GuB-rzaC6BN14AabsqCievoGh8Yw", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItNiIsImVtYWlsIjoidGVzdEBhcHRvc2xhYnMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlhdCI6MTcyNTQ3NTExMiwiZXhwIjoyNzAwMDAwMDAwLCJub25jZSI6IjcwOTUyNDIzMzM5NjQ0NTcyNjc5MzQ3MjM3NjgwODAzMDMzMjQ0NjI4MjExOTE3NTY0MDk0NTAwOTk1MTk3ODEwNTE5MTAxODcxMTgifQ.WoFAo8QOdYU5TWFxFo01n1ZfJaMlhbMb5cq82x0YvlBnA39ureqQ4f5kiB5Y0fMzqhJsfzVUq3lZBmCK5xYkhUUM-IoYhxW0R1efe7Hb1I1rD4OLwntxECSEvuftTqOYPDQRzatI1JORMrWYmRYsPlkgyrlnpq7GkVVd_gjlrTPKwMNTBxc6oXjgkjl7MqAir0KIo0gii-Yb_ApXHP17QpMjL5mVgqjYLDY3p5FeKRV3N6UyMCcIyWKDRs1qkeskhku-b52t-CU1jiaYtfzRI_bG_IMJjInT4QpFmaRr3sYS2sz1fX8oXSWxAxgbgc4nYqlAHJ5r6v-UYdBVGCA7yA", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItNyIsImVtYWlsIjoidGVzdEBhcHRvc2xhYnMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlhdCI6MTcyNTQ3NTExMiwiZXhwIjoyNzAwMDAwMDAwLCJub25jZSI6IjcwOTUyNDIzMzM5NjQ0NTcyNjc5MzQ3MjM3NjgwODAzMDMzMjQ0NjI4MjExOTE3NTY0MDk0NTAwOTk1MTk3ODEwNTE5MTAxODcxMTgifQ.Nz9PusTXc77ql77jZwMaA5qM69k38kRa7R9fKMIjpQ_ZFCyGa-d43cy6z0iqkjox9MAooNc1iE14aYAa-kFYGl8HREbmRD8o7pM4QaUi7J29jJcQ5xSTQQPH2skgQ3Yp32KsAFP1KvLtl2k9IGB5D-4uinpjxcfjFP55Xv50IkHLS4l3HoxBKpg7UzcFTfZ8hVsHwH7Z34LEb5HG7ZP-68gCTQjlDocZu5fXKOOZSVyQYQQ7cHni_y6c7YmWueDCCmZAbR1eqL9ttVGEmj5hzXDqsKPZuAlLf-nZgsIv3o3970D6mcgqdDy5qaRRm2Ch6Y9E2Z7pdNRImN-nY1mH6Q", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItOCIsImVtYWlsIjoidGVzdEBhcHRvc2xhYnMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlhdCI6MTcyNTQ3NTExMiwiZXhwIjoyNzAwMDAwMDAwLCJub25jZSI6IjcwOTUyNDIzMzM5NjQ0NTcyNjc5MzQ3MjM3NjgwODAzMDMzMjQ0NjI4MjExOTE3NTY0MDk0NTAwOTk1MTk3ODEwNTE5MTAxODcxMTgifQ.f1XRod-ZDlUcbkxAfgKZzSUTs9TuObPw0_K4EE9SsNUf9cDz976E3StiqNqtbuT5kWt_eb8sUsy82hlr0aZD5vMwlyM6J_SSp_lqsxZAKgmLdB07-YO0acdx0ElRNrFjVH8JlDKrPPhke83f4FU4lWN6EEnx6JqcNkh2YEA9r1FektH-54nc1ucjfBZwIgwC3PAex0mfYLq8E9g-cWNPlAZhVCcqdK0q0WfD4PBDrmUBREPYoAsZI6vmBx08m3OuhUJYTCTd4EFijlZcY8vdIxzXwe7VsZm3GeavzlsOpPjMoyPk06VvQtqvTlEm_yvTcjVrVXmWwkFr_v1-Mu_dZg", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItOSIsImVtYWlsIjoidGVzdEBhcHRvc2xhYnMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlhdCI6MTcyNTQ3NTExMiwiZXhwIjoyNzAwMDAwMDAwLCJub25jZSI6IjcwOTUyNDIzMzM5NjQ0NTcyNjc5MzQ3MjM3NjgwODAzMDMzMjQ0NjI4MjExOTE3NTY0MDk0NTAwOTk1MTk3ODEwNTE5MTAxODcxMTgifQ.nNETD85nln2HOqbJ1rQZ9keQM9TAwRJ0qqnUY_srBubPScowk9MxibHtUM8paDTKviGJY8G4GxCIb4A-Pk6CdgmbfRAyx8DMVf0Z-I_bfrVp64ZDU090fuZv7-uU0QgZYSvOMOibBz1oun-Ybuv5hqmruVq4OHmWcoB8fjofaV1GOauk7L4tAJ7nhCrBuZuPbBLCTMwYz7-0cLjPQ0L6bk9WcIW355Xu5w8hTuq-5ccI2zMyaFUmGA81-V5sVl_HTaX3x8X5JBi6JRL55De-BqQ8-TzJW44i2WU-gC9p6SeHnPXdQESjf8YdRmSumnupk5aoMOtYr2OVdzFM0O06SQ", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItMTAiLCJlbWFpbCI6InRlc3RAYXB0b3NsYWJzLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpYXQiOjE3MjU0NzUxMTIsImV4cCI6MjcwMDAwMDAwMCwibm9uY2UiOiI3MDk1MjQyMzMzOTY0NDU3MjY3OTM0NzIzNzY4MDgwMzAzMzI0NDYyODIxMTkxNzU2NDA5NDUwMDk5NTE5NzgxMDUxOTEwMTg3MTE4In0.i_awHcWeU39k9no2klEajvTevA95naoquvzKu2DrS9lS_7Fm52pezCSb2e3M1ovvkoITpTb5IfRCo2PrP8AuGUv2srZU3GqDoyZaMYguvKJLLeqV670i4knGFWqfUegAJh1VB_-yN4nlbV9SXG7D7yk0WwWEUUGfWB7_NGqsHX2B84f_U_Dus14x7za7Y1t91EeXhzwCgZJMB0DfXT2Y3VUTU1O1abUKJ866V5pdQC7HJv2qUthmn-TlSIl6qac5dQQ14YxQtyEKXeTP74guxArcdvCsPuC8TAAnP_f_NxehFRRSQ-HJ3tJ5ym7PwQ4L1uaW5Rxc7duqsrk1JdX-zA", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItMTEiLCJlbWFpbCI6InRlc3RAYXB0b3NsYWJzLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpYXQiOjE3MjU0NzUxMTIsImV4cCI6MjcwMDAwMDAwMCwibm9uY2UiOiI3MDk1MjQyMzMzOTY0NDU3MjY3OTM0NzIzNzY4MDgwMzAzMzI0NDYyODIxMTkxNzU2NDA5NDUwMDk5NTE5NzgxMDUxOTEwMTg3MTE4In0.Z_5NEdbvnP9kqsCrOPIqCrR8iYX9kCIFsCMFiU9ozmkNjCDHgU5XQKuYRfsi7Mfq1CQL7gZVtJsDXWmv6ENBRJFFu9BHORuTZrMZi-on5yVZVf4J1RaxcRAdFOCIznN6DT6DyAFxq0vyCutUI76BtmYr17g-SUrESUkgG4jgL0xIcMNcitKWQ35o8fYJWeaTAa2x3SbX1IgCAFdjsD6DLm-LY5t7MicHekrpkHDBv_PuP9xzxRbNJfGnNnnB41h9sfATqiIMJXLvUfuDKmN43MQAowODTtgDfnuLNwBuXkGOCLQmtLjdNYqLRGHccFwEsanfUimOCmsjRMvJSt1o7A", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItMTIiLCJlbWFpbCI6InRlc3RAYXB0b3NsYWJzLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpYXQiOjE3MjU0NzUxMTIsImV4cCI6MjcwMDAwMDAwMCwibm9uY2UiOiI3MDk1MjQyMzMzOTY0NDU3MjY3OTM0NzIzNzY4MDgwMzAzMzI0NDYyODIxMTkxNzU2NDA5NDUwMDk5NTE5NzgxMDUxOTEwMTg3MTE4In0.Yq69UbPUVLTG6IMnqwiqf-sMnm42JMdTfOIv-8bjQfIPRQauofFeqEYY5ls6i41DbU9dLkMofoOshm04NI2zk0W3lE-6USak4Tq2xnnWYsPuWlXV7P1w7f6_KwPD8BYnGiWw5DTZVG4tjn3El9D8lPikkWlEAE0Z-aR1p3ezwzqIoRokeN4A7uxvcAzTWSqYQ-JkF_GAMbnLScfjXVEpCjiDnF-ydW3Aj6B87S8K_EWOKptFpswS1UVCuwVDm1wGouvK9WbBSAdtZhdl95p_geke_l53z3PZdjYMYrmtTSxJdBAL-iJ-UpODqzz8sh9rGXBv70wM14oW19IboX9q7g", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItMTMiLCJlbWFpbCI6InRlc3RAYXB0b3NsYWJzLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpYXQiOjE3MjU0NzUxMTIsImV4cCI6MjcwMDAwMDAwMCwibm9uY2UiOiI3MDk1MjQyMzMzOTY0NDU3MjY3OTM0NzIzNzY4MDgwMzAzMzI0NDYyODIxMTkxNzU2NDA5NDUwMDk5NTE5NzgxMDUxOTEwMTg3MTE4In0.ZT_zGYMcj4hyzIAyqE3VAte_pxiPxF1cnUBpgcSl7NCIRCO3ErpmsXlvas438UO03tj8mqpyuwFuTEHjSAVZRSU_rOgGowtTnwBRBJJH3pFzvv3JE3mnRro_7nQWXRZqYzmUNAxzEEhn5fJQGyYXilT7Rc-5E9CHtcjxnpN-4eC9oNfQiw4hOG5nzx54PxQwirlLfS0l0zc4-qRmS31vNJBKIULD_6y0MUrr94dZk-M6fBGqGVuCLgMfh3jlCFgMjpE5BBBCvoodRu5mJ8aNWa6DgqHUlc3LXqL_Q9IezFS4KIP2KYASe10sRVRs16_dZXy_ng5Ny9shzNRKMZiNCw", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItMTQiLCJlbWFpbCI6InRlc3RAYXB0b3NsYWJzLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpYXQiOjE3MjU0NzUxMTIsImV4cCI6MjcwMDAwMDAwMCwibm9uY2UiOiI3MDk1MjQyMzMzOTY0NDU3MjY3OTM0NzIzNzY4MDgwMzAzMzI0NDYyODIxMTkxNzU2NDA5NDUwMDk5NTE5NzgxMDUxOTEwMTg3MTE4In0.PDY8M4wnfh1gQ-DLnozLekWRCIJAL6y-8ceK7tlrFcLaoiNfnP2pPfgY-cUKAV7hEkKAt89PTUXOa_d0JUmjh0XkZUn28KqXK229Mnp-wUE114GD-Bu1rlQLQoJ5m_sWWLBEV4_FlKx0igyJSWb0YYYl5BfgxDGoyCnQTfXXC4Ha6OJQo88qcQD_K-PRmZ558fhAWrRJaeukegbXpGfw6Hhyw4z73GhYvEiupXuGB34UiWxWsTHyH9EzY61wIQQLGs0qjXMa2QMDEnApqWYnVWcug86IHz5JX5fxxmVz9qzMufc-WArplFnD5LLulZdg9zQdXODcyEyRZbtuBfEcdw", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItMTUiLCJlbWFpbCI6InRlc3RAYXB0b3NsYWJzLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpYXQiOjE3MjU0NzUxMTIsImV4cCI6MjcwMDAwMDAwMCwibm9uY2UiOiI3MDk1MjQyMzMzOTY0NDU3MjY3OTM0NzIzNzY4MDgwMzAzMzI0NDYyODIxMTkxNzU2NDA5NDUwMDk5NTE5NzgxMDUxOTEwMTg3MTE4In0.cVzffagURHwPU-5c-NYAKoPF9_43d74ashqvCOzbWGL7PyQY_mNZs-2oev_QHXyrjl4PkmCvviiaawTKyQP9gWFFVfnyQYicP8M1gCvVUKenPYLpzYAp8SEUCISyKkDE3qPc73pf2FQZDl6815edBKiVaIoomdoJs_7QE28Ipo4d8D1g61cetE5sc_JMYDWg_PpeTdB5vBPuPKzr8Mc1zz9u8AJhLn8c-XaDDubhvwj36xsgVY3nw61FQxW9SBwLoTmoTakV57SP49Z2glzMrY5hsfrXyWynkJQadg-ZiJbpoqXfrXINdGr7y9Iboh2g18D7Cu3vlFpv_PQx6cYJZQ", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItMTYiLCJlbWFpbCI6InRlc3RAYXB0b3NsYWJzLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpYXQiOjE3MjU0NzUxMTIsImV4cCI6MjcwMDAwMDAwMCwibm9uY2UiOiI3MDk1MjQyMzMzOTY0NDU3MjY3OTM0NzIzNzY4MDgwMzAzMzI0NDYyODIxMTkxNzU2NDA5NDUwMDk5NTE5NzgxMDUxOTEwMTg3MTE4In0.WEDo0H753toeCM9OnbeTL5ofJAAmjv7crP9FHrGA_YcPRnfMdmnjpa3cI4olloSDI2j69BGOK05CEjumCXjz-YPoZDgiEuJtDSEZA-1CRUXTMvbERjK47o2X82T_iVLRBVihDEbmyxAnCnwPNLTGMMr2-2o2RCzlHhOTryih-7CNj6ty9HMlOmlIA12FWj5Ik8G0BdDaWhBClaGh9UYiDKXWUvBLFVD_p8FA-GJ6D9ivwj10HIMHnYxCNxDMxBYytS0Kr64AJ6Pe5Chrhq2u-XlrnVDJVWmgoMlsoMAZ2EVzVOU_s_cHK_7QIaySaSTEO-rkdhFMulqYMHDc9JWkpA", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItMTciLCJlbWFpbCI6InRlc3RAYXB0b3NsYWJzLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpYXQiOjE3MjU0NzUxMTIsImV4cCI6MjcwMDAwMDAwMCwibm9uY2UiOiI3MDk1MjQyMzMzOTY0NDU3MjY3OTM0NzIzNzY4MDgwMzAzMzI0NDYyODIxMTkxNzU2NDA5NDUwMDk5NTE5NzgxMDUxOTEwMTg3MTE4In0.WtLlWEKtNeI2HmYdIJam_bWLGxvOVTSj4ohxJuB1NBfSZxQbuAgQ3_pnXVd7uioQyUm5JEloG4FkJFYE7hmrlL5ocCqufws40HQJQx2bJm_8dePccMNRyjsk7Ya7_1dp0d5QEH-Ckob_MxIAawHqhMUGTExVXuk83STXCVN3e9HHzo7UiekuYIaAbSlBpX_ySkfAPDHvkKpXwg25w9zI3y0JrVGwoKlGNzwasFiY0RVtLNLQPpjz_LEdH9xappg36M75SP-q0lJO5KaWtmNHHTTQ11GXs3xXwMCn9mU6kk_sBy3N_GVtkSP_qjCAdHYKUPhx4Pe1Xajb_trvvCCk9g", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItMTgiLCJlbWFpbCI6InRlc3RAYXB0b3NsYWJzLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpYXQiOjE3MjU0NzUxMTIsImV4cCI6MjcwMDAwMDAwMCwibm9uY2UiOiI3MDk1MjQyMzMzOTY0NDU3MjY3OTM0NzIzNzY4MDgwMzAzMzI0NDYyODIxMTkxNzU2NDA5NDUwMDk5NTE5NzgxMDUxOTEwMTg3MTE4In0.CFK5gunymQ7Jf7stOsWdEVO1y275eSefL0LsQku-Gdn_iWVFmzY_ASQqBJt8jmfM17zht5NUrASm095yn3vDRYnx-CgtDwJrpF3jeLbt101ld8VytJxng4CjyznzrVuSPLM4DMiUjILG4vd9wNOkEUgPQzu5qdqEGckxj-c1lMpWhyWDR9pZaX_BryNGNdQvw4auJFuHcWl4HeVLYhdcZulbz_OAq1huYZgrjzDgVrMjXOxo2SF6ObmnJzrRllrynwingLAsGQtiNWppVNcpeG-DxFhWYhnb_w-oTk2HcwFoecWr0R_ran_UY1n0aPZ_2MzZ_NsIaeG0VyIk0SxbHw", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InRlc3QtcnNhIn0.eyJpc3MiOiJ0ZXN0LmZlZGVyYXRlZC5vaWRjLnByb3ZpZGVyIiwiYXVkIjoidGVzdC1rZXlsZXNzLWRhcHAiLCJzdWIiOiJ0ZXN0LXVzZXItMTkiLCJlbWFpbCI6InRlc3RAYXB0b3NsYWJzLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJpYXQiOjE3MjU0NzUxMTIsImV4cCI6MjcwMDAwMDAwMCwibm9uY2UiOiI3MDk1MjQyMzMzOTY0NDU3MjY3OTM0NzIzNzY4MDgwMzAzMzI0NDYyODIxMTkxNzU2NDA5NDUwMDk5NTE5NzgxMDUxOTEwMTg3MTE4In0.cOZY1MKhMYDv6GNAI61ojFKtZS6M1a_5eOkuVVVFk1aEpvMqe12irXy8WZOxSauIcZvy9CazXPZprkEhnGIExV4LFzbelG4T2DP7cvO9zmJXb481p3g5U0tWfahvh5EqZp9j3fxvKGcEYNA8CH3rFpYwltCxk_yKNkQFJvXcUOyLbu9t98HBvhcl2-vL-g61K7f9v_ZEvRuhJFbGNXRY4GZdvEuzsKacNcYOMPELptoEZpgeHvilfqPtIGhQJbl5f8m4etXfHlC29N7dAd3avkiqfE_4zpUGe9t8ZGonpZkzvBgsknK_g7pCwI8SstBKJJm6s9etbbmQJsHO1mwLdw", +]; + const KEYLESS_TEST_TIMEOUT = 12000; describe("keyless api", () => { const ephemeralKeyPair = EPHEMERAL_KEY_PAIR; - // TODO: Make this work for local by spinning up a local proving service. const { aptos } = getAptosClient(); + const jwkAccount = Account.generate(); + + beforeAll(async () => { + await aptos.fundAccount({ + accountAddress: jwkAccount.accountAddress, + amount: FUND_AMOUNT, + }); + const jwkTransaction = await aptos.updateFederatedKeylessJwkSetTransaction({ + sender: jwkAccount, + iss: "test.federated.oidc.provider", + jwksUrl: "https://github.com/aptos-labs/aptos-core/raw/main/types/src/jwks/rsa/secure_test_jwk.json", + }); + const committedJwkTxn = await aptos.signAndSubmitTransaction({ signer: jwkAccount, transaction: jwkTransaction }); + await aptos.waitForTransaction({ transactionHash: committedJwkTxn.hash }); + }); + + test( + "installs jwks for an auth0 iss", + async () => { + const sender = Account.generate(); + await aptos.fundAccount({ + accountAddress: sender.accountAddress, + amount: FUND_AMOUNT, + }); + const jwkTransaction = await aptos.updateFederatedKeylessJwkSetTransaction({ + sender, + iss: "https://dev-qtdgjv22jh0v1k7g.us.auth0.com/", + }); + const committedJwkTxn = await aptos.signAndSubmitTransaction({ signer: sender, transaction: jwkTransaction }); + await aptos.waitForTransaction({ transactionHash: committedJwkTxn.hash }); + }, + KEYLESS_TEST_TIMEOUT, + ); + + describe.each([ + { jwts: TEST_JWT_TOKENS, jwkAddress: undefined }, + { jwts: TEST_FEDERATED_JWT_TOKENS, jwkAddress: jwkAccount.accountAddress }, + ])("keyless account", ({ jwts, jwkAddress }) => { + let i = 0; + let jwt: string; + beforeEach(async () => { + jwt = jwts[i % jwts.length]; + i += 1; + }); - describe("keyless account", () => { test( "derives the keyless account and submits a transaction", async () => { - // Select a random test token. Using the same one may encounter rate limits - const jwt = TEST_JWT_TOKENS[Math.floor(Math.random() * TEST_JWT_TOKENS.length)]; - const account = await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair }); + const sender = + jwkAddress === undefined + ? await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair }) + : await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, jwkAddress }); const recipient = Account.generate(); - await simpleCoinTransactionHelper(aptos, account, recipient); + await simpleCoinTransactionHelper(aptos, sender, recipient); }, KEYLESS_TEST_TIMEOUT, ); @@ -53,16 +126,20 @@ describe("keyless api", () => { test( "creates the keyless account via the static constructor and submits a transaction", async () => { - const jwt = TEST_JWT_TOKENS[0]; - const pepper = await aptos.getPepper({ jwt, ephemeralKeyPair }); - const publicKey = KeylessPublicKey.fromJwtAndPepper({ jwt, pepper }); + const publicKey = + jwkAddress === undefined + ? KeylessPublicKey.fromJwtAndPepper({ jwt, pepper }) + : FederatedKeylessPublicKey.fromJwtAndPepper({ jwt, pepper, jwkAddress }); const address = await aptos.lookupOriginalAccountAddress({ authenticationKey: publicKey.authKey().derivedAddress(), }); const proof = await aptos.getProof({ jwt, ephemeralKeyPair, pepper }); - const account = KeylessAccount.create({ address, proof, jwt, ephemeralKeyPair, pepper }); + const account = + jwkAddress === undefined + ? KeylessAccount.create({ address, proof, jwt, ephemeralKeyPair, pepper }) + : FederatedKeylessAccount.create({ address, proof, jwt, ephemeralKeyPair, pepper, jwkAddress }); const recipient = Account.generate(); await simpleCoinTransactionHelper(aptos, account, recipient); }, @@ -72,9 +149,10 @@ describe("keyless api", () => { test( "derives the keyless account with email uidKey and submits a transaction", async () => { - // Select a random test token. Using the same one may encounter rate limits - const jwt = TEST_JWT_TOKENS[Math.floor(Math.random() * TEST_JWT_TOKENS.length)]; - const sender = await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, uidKey: "email" }); + const sender = + jwkAddress === undefined + ? await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, uidKey: "email" }) + : await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, jwkAddress, uidKey: "email" }); const recipient = Account.generate(); await simpleCoinTransactionHelper(aptos, sender, recipient); }, @@ -84,9 +162,10 @@ describe("keyless api", () => { test( "derives the keyless account with custom pepper and submits a transaction", async () => { - // Select a random test token. Using the same one may encounter rate limits - const jwt = TEST_JWT_TOKENS[Math.floor(Math.random() * TEST_JWT_TOKENS.length)]; - const sender = await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, pepper: new Uint8Array(31) }); + const sender = + jwkAddress === undefined + ? await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, pepper: new Uint8Array(31) }) + : await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, jwkAddress, pepper: new Uint8Array(31) }); const recipient = Account.generate(); await simpleCoinTransactionHelper(aptos, sender, recipient); }, @@ -96,8 +175,6 @@ describe("keyless api", () => { test( "deriving keyless account with async proof fetch executes callback", async () => { - // Select a random test token. Using the same one may encounter rate limits - const jwt = TEST_JWT_TOKENS[Math.floor(Math.random() * TEST_JWT_TOKENS.length)]; let succeeded = false; const proofFetchCallback = async (res: ProofFetchStatus) => { if (res.status === "Failed") { @@ -105,7 +182,10 @@ describe("keyless api", () => { } succeeded = true; }; - const sender = await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, proofFetchCallback }); + const sender = + jwkAddress === undefined + ? await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, proofFetchCallback }) + : await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, proofFetchCallback, jwkAddress }); expect(succeeded).toBeFalsy(); await sender.waitForProofFetch(); expect(succeeded).toBeTruthy(); @@ -118,10 +198,11 @@ describe("keyless api", () => { test( "derives the keyless account with async proof fetch and submits a transaction", async () => { - // Select a random test token. Using the same one may encounter rate limits - const jwt = TEST_JWT_TOKENS[Math.floor(Math.random() * TEST_JWT_TOKENS.length)]; const proofFetchCallback = async () => {}; - const sender = await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, proofFetchCallback }); + const sender = + jwkAddress === undefined + ? await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, proofFetchCallback }) + : await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, proofFetchCallback, jwkAddress }); await aptos.fundAccount({ accountAddress: sender.accountAddress, amount: FUND_AMOUNT, @@ -140,10 +221,11 @@ describe("keyless api", () => { test( "deriving keyless account with async proof fetch throws when trying to immediately sign", async () => { - // Select a random test token. Using the same one may encounter rate limits - const jwt = TEST_JWT_TOKENS[Math.floor(Math.random() * TEST_JWT_TOKENS.length)]; const proofFetchCallback = async () => {}; - const sender = await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, proofFetchCallback }); + const sender = + jwkAddress === undefined + ? await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, proofFetchCallback }) + : await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, proofFetchCallback, jwkAddress }); await aptos.fundAccount({ accountAddress: sender.accountAddress, amount: FUND_AMOUNT, @@ -163,16 +245,25 @@ describe("keyless api", () => { test( "deriving keyless account using all parameters", async () => { - // Select a random test token. Using the same one may encounter rate limits - const jwt = TEST_JWT_TOKENS[Math.floor(Math.random() * TEST_JWT_TOKENS.length)]; const proofFetchCallback = async () => {}; - const sender = await aptos.deriveKeylessAccount({ - jwt, - ephemeralKeyPair, - uidKey: "email", - pepper: new Uint8Array(31), - proofFetchCallback, - }); + + const sender = + jwkAddress === undefined + ? await aptos.deriveKeylessAccount({ + jwt, + ephemeralKeyPair, + uidKey: "email", + pepper: new Uint8Array(31), + proofFetchCallback, + }) + : await aptos.deriveKeylessAccount({ + jwt, + ephemeralKeyPair, + uidKey: "email", + pepper: new Uint8Array(31), + proofFetchCallback, + jwkAddress, + }); const recipient = Account.generate(); await simpleCoinTransactionHelper(aptos, sender, recipient); }, @@ -182,9 +273,10 @@ describe("keyless api", () => { test( "simulation works correctly", async () => { - // Select a random test token. Using the same one may encounter rate limits - const jwt = TEST_JWT_TOKENS[Math.floor(Math.random() * TEST_JWT_TOKENS.length)]; - const sender = await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair }); + const sender = + jwkAddress === undefined + ? await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair }) + : await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, jwkAddress }); await aptos.fundAccount({ accountAddress: sender.accountAddress, amount: FUND_AMOUNT, @@ -202,9 +294,10 @@ describe("keyless api", () => { test( "keyless account verifies signature for arbitrary message correctly", async () => { - // Select a random test token. Using the same one may encounter rate limits - const jwt = TEST_JWT_TOKENS[Math.floor(Math.random() * TEST_JWT_TOKENS.length)]; - const sender = await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair }); + const sender = + jwkAddress === undefined + ? await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair }) + : await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, jwkAddress }); const message = "hello world"; const signature = sender.sign(message); expect(sender.verifySignature({ message, signature })).toBe(true); @@ -215,11 +308,13 @@ describe("keyless api", () => { test( "serializes and deserializes", async () => { - // Select a random test token. Using the same one may encounter rate limits - const jwt = TEST_JWT_TOKENS[Math.floor(Math.random() * TEST_JWT_TOKENS.length)]; - const sender = await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair }); + const sender = + jwkAddress === undefined + ? await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair }) + : await aptos.deriveKeylessAccount({ jwt, ephemeralKeyPair, jwkAddress }); const bytes = sender.bcsToBytes(); - const deserializedAccount = KeylessAccount.fromBytes(bytes); + const deserializedAccount = + jwkAddress === undefined ? KeylessAccount.fromBytes(bytes) : FederatedKeylessAccount.fromBytes(bytes); expect(bytes).toEqual(deserializedAccount.bcsToBytes()); }, KEYLESS_TEST_TIMEOUT,