Skip to content

Commit 1ec83aa

Browse files
authored
refactor: changes JWT signing alg to ES256KADR36 (#110)
1 parent 0558f18 commit 1ec83aa

File tree

12 files changed

+99
-229
lines changed

12 files changed

+99
-229
lines changed

ts/README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,12 @@ This is the recommended method for getting authorized access to your resources o
100100
**Generating a JWT Token**
101101

102102
```ts
103-
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
104-
import { JwtTokenManager, createSignArbitraryAkashWallet } from "@akashnetwork/chain-sdk"
103+
import { Secp256k1HdWallet } from "@cosmjs/amino";
104+
import { JwtTokenManager } from "@akashnetwork/chain-sdk"
105105

106-
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: "akash" });
106+
const wallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, { prefix: "akash" });
107107
const accounts = await wallet.getAccounts();
108-
const signer = await createSignArbitraryAkashWallet(wallet);
109-
const tokenManager = new JwtTokenManager(signer);
108+
const tokenManager = new JwtTokenManager(wallet);
110109

111110
// See https://akash.network/roadmap/aep-64/ for details
112111
const token = await tokenManager.generateToken({

ts/package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ts/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"@cosmjs/math": "^0.33.1",
6161
"@cosmjs/proto-signing": "^0.33.1",
6262
"@cosmjs/stargate": "^0.33.1",
63+
"base64-js": "^1.5.1",
6364
"js-yaml": "^4.1.0",
6465
"json-stable-stringify": "^1.3.0",
6566
"jsrsasign": "^11.1.0",

ts/src/generated/protos/akash/base/offchain/sign/v1/sign.ts

Lines changed: 0 additions & 144 deletions
This file was deleted.

ts/src/sdk/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ export * from "./index.shared.ts";
22
export { createChainNodeSDK, type ChainNodeSDKOptions } from "./chain/createChainNodeSDK.ts";
33
export { createProviderSDK, type ProviderSDKOptions } from "./provider/createProviderSDK.ts";
44
export { certificateManager, CertificateManager, type CertificateInfo, type CertificatePem, type ValidityRangeOptions } from "./provider/auth/mtls/index.ts";
5-
export { JwtTokenManager, type CreateJWTOptions, type JwtTokenPayload, type JwtValidationResult, createSignArbitraryAkashWallet, type SignArbitraryAkashWallet } from "./provider/auth/jwt/index.ts";
5+
export * from "./provider/auth/jwt/index.ts";

ts/src/sdk/index.web.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export * from "./index.shared.ts";
22
export { createChainNodeWebSDK, type ChainNodeWebSDKOptions } from "./chain/createChainNodeWebSDK.ts";
33
export { certificateManager, CertificateManager, type CertificateInfo, type CertificatePem, type ValidityRangeOptions } from "./provider/auth/mtls/index.ts";
4-
export { JwtTokenManager, type CreateJWTOptions, type JwtTokenPayload, type JwtValidationResult, type SignArbitraryAkashWallet } from "./provider/auth/jwt/index.ts";
4+
export * from "./provider/auth/jwt/index.ts";

ts/src/sdk/provider/auth/jwt/base64.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { fromByteArray } from "base64-js";
2+
13
export function base64UrlEncode(value: string | Uint8Array): string {
2-
const str = typeof value === "string" ? value : String.fromCharCode(...value);
3-
const base64 = btoa(str);
4-
return toBase64Url(base64);
4+
return toBase64Url(base64Encode(value));
55
}
66

77
/**
@@ -11,13 +11,14 @@ export function toBase64Url(base64Encoded: string): string {
1111
return base64Encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1212
}
1313

14+
const textDecoder = new TextDecoder();
1415
export function base64UrlDecode(value: string): string {
1516
let str = value;
1617
// Convert from base64url → base64
1718
str = str.replace(/-/g, "+").replace(/_/g, "/");
1819
str = str.padEnd(str.length + (4 - (str.length % 4)) % 4, "=");
1920

20-
return new TextDecoder().decode(Uint8Array.from(atob(str), (c) => c.charCodeAt(0)));
21+
return textDecoder.decode(Uint8Array.from(atob(str), (c) => c.charCodeAt(0)));
2122
}
2223

2324
/**
@@ -29,3 +30,9 @@ export function base64Decode(base64String: string): Record<string, unknown> {
2930
const decoded = atob(base64String);
3031
return JSON.parse(decoded);
3132
}
33+
34+
const textEncoder = new TextEncoder();
35+
export function base64Encode(value: string | Uint8Array): string {
36+
const data = typeof value === "string" ? textEncoder.encode(value) : value;
37+
return fromByteArray(data);
38+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export { JwtTokenManager, type CreateJWTOptions } from "./jwt-token.ts";
22
export type { JwtTokenPayload, AccessScope as JwtTokenAccessScope, LeasePermission } from "./types.ts";
33
export { JwtValidator, type JwtValidationResult } from "./jwt-validator.ts";
4-
export { createSignArbitraryAkashWallet, type SignArbitraryAkashWallet } from "./wallet-utils.ts";
4+
export { type OfflineDataSigner } from "./wallet-utils.ts";

ts/src/sdk/provider/auth/jwt/jwt-token.spec.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
1+
import { Secp256k1HdWallet } from "@cosmjs/amino";
2+
import type { AccountData } from "@cosmjs/proto-signing";
23
import { beforeAll, describe, expect, it } from "@jest/globals";
34
import fs from "fs";
45
import path from "path";
@@ -8,28 +9,29 @@ import type { CreateJWTOptions } from "./jwt-token.ts";
89
import { JwtTokenManager } from "./jwt-token.ts";
910
import type { ClaimsTestCase, SigningTestCase } from "./test/test-utils.ts";
1011
import { replaceTemplateValues } from "./test/test-utils.ts";
11-
import { createSignArbitraryAkashWallet, type SignArbitraryAkashWallet } from "./wallet-utils.ts";
12+
import { createOfflineDataSigner } from "./wallet-utils.ts";
1213

1314
describe("JWT Claims Validation", () => {
1415
const testdataPath = path.join(__dirname, "../../../../../..", "testdata", "jwt");
1516
const jwtMnemonic = fs.readFileSync(path.join(testdataPath, "mnemonic"), "utf-8").trim();
1617
const jwtSigningTestCases = JSON.parse(fs.readFileSync(path.join(testdataPath, "cases_es256k.json"), "utf-8")) as SigningTestCase[];
1718
const jwtClaimsTestCases = JSON.parse(fs.readFileSync(path.join(testdataPath, "cases_jwt.json.tmpl"), "utf-8")) as ClaimsTestCase[];
1819

19-
let testWallet: DirectSecp256k1HdWallet;
20+
let testWallet: Secp256k1HdWallet;
2021
let jwtToken: JwtTokenManager;
21-
let akashWallet: SignArbitraryAkashWallet;
22+
let testAccount: AccountData;
2223

2324
beforeAll(async () => {
24-
testWallet = await DirectSecp256k1HdWallet.fromMnemonic(jwtMnemonic, {
25+
testWallet = await Secp256k1HdWallet.fromMnemonic(jwtMnemonic, {
2526
prefix: "akash",
2627
});
27-
akashWallet = await createSignArbitraryAkashWallet(testWallet);
28-
jwtToken = new JwtTokenManager(akashWallet);
28+
const [account] = await testWallet.getAccounts();
29+
testAccount = account;
30+
jwtToken = new JwtTokenManager(testWallet);
2931
});
3032

31-
it.each(jwtClaimsTestCases)("$description", async (testCase) => {
32-
const { claims, tokenString } = replaceTemplateValues(testCase);
33+
it.each(jwtClaimsTestCases.filter(isSigningWithES256KADR36))("$description", async (testCase) => {
34+
const { claims, tokenString } = replaceTemplateValues(testCase, { iss: testAccount.address });
3335

3436
// For test cases that should fail, we need to validate the payload first
3537
if (testCase.expected.signFail || testCase.expected.verifyFail) {
@@ -54,16 +56,17 @@ describe("JWT Claims Validation", () => {
5456
}
5557
});
5658

57-
it.each(jwtSigningTestCases)("$description", async (testCase) => {
59+
it.each(jwtSigningTestCases.filter(isSigningWithES256KADR36))("$description", async (testCase) => {
5860
const [expectedHeader, expectedPayload, expectedSignature] = testCase.tokenString.split(".");
5961
expect(expectedHeader).toBeDefined();
6062
expect(expectedPayload).toBeDefined();
6163
expect(expectedSignature).toBeDefined();
6264

6365
const signingString = `${expectedHeader}.${expectedPayload}`;
6466

65-
// Sign using the mock wallet's signArbitrary method
66-
const signResponse = await akashWallet.signArbitrary(akashWallet.address, signingString);
67+
const signer = createOfflineDataSigner(testWallet);
68+
const [account] = await testWallet.getAccounts();
69+
const signResponse = await signer.signArbitrary(account.address, signingString);
6770
const signature = toBase64Url(signResponse.signature);
6871

6972
if (!testCase.mustFail) {
@@ -72,4 +75,8 @@ describe("JWT Claims Validation", () => {
7275
expect(signature).not.toBe(expectedSignature);
7376
}
7477
});
78+
79+
function isSigningWithES256KADR36(testCase: SigningTestCase | ClaimsTestCase): boolean {
80+
return !testCase.expected.alg || testCase.expected.alg === "ES256KADR36";
81+
}
7582
});

ts/src/sdk/provider/auth/jwt/jwt-token.ts

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,36 @@
1+
import type { OfflineAminoSigner } from "@cosmjs/amino";
2+
import { default as stableStringify } from "json-stable-stringify";
3+
14
import { base64UrlDecode, base64UrlEncode, toBase64Url } from "./base64.ts";
25
import { JwtValidator } from "./jwt-validator.ts";
36
import type { JwtTokenPayload } from "./types.ts";
4-
import type { SignArbitraryAkashWallet } from "./wallet-utils.ts";
7+
import type { OfflineDataSigner } from "./wallet-utils.ts";
8+
import { createOfflineDataSigner } from "./wallet-utils.ts";
59

610
export class JwtTokenManager {
7-
private validator: JwtValidator;
8-
private wallet: SignArbitraryAkashWallet;
11+
private readonly validator: JwtValidator;
12+
private readonly signer: OfflineDataSigner;
913

10-
constructor(wallet: SignArbitraryAkashWallet) {
14+
constructor(signer: OfflineDataSigner | OfflineAminoSigner) {
1115
this.validator = new JwtValidator();
12-
this.wallet = wallet;
16+
this.signer = "signAmino" in signer ? createOfflineDataSigner(signer) : signer;
1317
}
1418

1519
/**
1620
* Creates a new JWT token with ES256K signature using a custom signArbitrary method with the current wallet
1721
* @param options - JWT token options
1822
* @returns The signed JWT token
1923
* @example
20-
* const wallet = await DirectSecp256k1HdWallet.fromMnemonic(jwtMnemonic, {
24+
* const wallet = await Secp256k1HdWallet.fromMnemonic(jwtMnemonic, {
2125
* prefix: "akash"
2226
* });
23-
* const akashWallet = await createSignArbitraryAkashWallet(wallet);
24-
* const jwtToken = new JwtToken(akashWallet);
27+
* const jwtToken = new JwtTokenManager(wallet);
2528
* // OR ON FRONTEND
26-
* const { getAccount, signArbitrary } = useSelectedChain();
27-
* const { address, pubkey } = await getAccount();
28-
* const jwt = new JwtToken(
29-
* {
30-
* signArbitrary,
31-
* address,
32-
* pubkey
33-
* }
34-
* );
29+
* const wallet = useSelectedChain();
30+
* const jwt = new JwtTokenManager(wallet);
3531
* const token = await jwtToken.generateToken({
3632
* version: "v1",
37-
* iss: "akash...",
33+
* iss: wallet.address, // akash1...
3834
* exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
3935
* iat: Math.floor(Date.now() / 1000), // current timestamp
4036
* });
@@ -57,9 +53,9 @@ export class JwtTokenManager {
5753
throw new Error(`Invalid payload: ${validationResult.errors?.join(", ")}`);
5854
}
5955

60-
const header = base64UrlEncode(JSON.stringify({ alg: "ES256K", typ: "JWT" }));
61-
const stringPayload = base64UrlEncode(JSON.stringify(inputPayload));
62-
const { signature } = await this.wallet.signArbitrary(this.wallet.address, `${header}.${stringPayload}`);
56+
const header = base64UrlEncode(stableStringify({ alg: this.signer.algorithm || "ES256KADR36", typ: "JWT" })!);
57+
const stringPayload = base64UrlEncode(stableStringify(inputPayload)!);
58+
const { signature } = await this.signer.signArbitrary(options.iss, `${header}.${stringPayload}`);
6359
const token = `${header}.${stringPayload}.${toBase64Url(signature)}`;
6460

6561
return token;

0 commit comments

Comments
 (0)