Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/core/service/auth/dashboard-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ function signNonce(keypair: typeof TEST_KEYPAIR, nonce: string): string {
return sigBuffer.toString("base64");
}

async function signNonceSep53(keypair: typeof TEST_KEYPAIR, nonce: string): Promise<string> {
const prefix = "Stellar Signed Message:\n";
const prefixedMessage = Buffer.concat([
Buffer.from(prefix, "utf-8"),
Buffer.from(nonce, "utf-8"),
]);
const hash = Buffer.from(
await crypto.subtle.digest("SHA-256", prefixedMessage),
);
const sigBuffer = keypair.sign(hash);
return sigBuffer.toString("hex");
}

Deno.test("createDashboardChallenge - returns a nonce", () => {
const { nonce } = createDashboardChallenge(TEST_PUBLIC_KEY);
assertEquals(typeof nonce, "string");
Expand Down Expand Up @@ -109,6 +122,32 @@ Deno.test("verifyDashboardChallenge - valid signature + different provider (no H
);
});

Deno.test("verifyDashboardChallenge - SEP-53 hex signature + self signer = success", async () => {
const { nonce } = createDashboardChallenge(TEST_PUBLIC_KEY);
const signature = await signNonceSep53(TEST_KEYPAIR, nonce);

const { token } = await verifyDashboardChallenge(
nonce,
signature,
TEST_PUBLIC_KEY,
SELF_SIGNER_CONFIG,
);

assertEquals(typeof token, "string");
assertEquals(token.length > 0, true);
});

Deno.test("verifyDashboardChallenge - SEP-53 wrong key rejected", async () => {
const { nonce } = createDashboardChallenge(TEST_PUBLIC_KEY);
const signature = await signNonceSep53(Keypair.random(), nonce);

await assertRejects(
() => verifyDashboardChallenge(nonce, signature, TEST_PUBLIC_KEY, SELF_SIGNER_CONFIG),
Error,
"Invalid signature",
);
});

Deno.test("verifyDashboardChallenge - nonce is consumed after use", async () => {
const { nonce } = createDashboardChallenge(TEST_PUBLIC_KEY);
const signature = signNonce(TEST_KEYPAIR, nonce);
Expand Down
31 changes: 26 additions & 5 deletions src/core/service/auth/dashboard-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,35 @@ export async function verifyDashboardChallenge(
// Consume the challenge (one-time use)
pendingChallenges.delete(nonce);

// 2. Verify Ed25519 signature
// 2. Verify Ed25519 signature (supports both SEP-53 and raw formats)
span.addEvent("verifying_signature");
try {
const keypair = Keypair.fromPublicKey(publicKey);
const nonceBuffer = Buffer.from(nonce, "base64");
const sigBuffer = Buffer.from(signature, "base64");
if (!keypair.verify(nonceBuffer, sigBuffer)) {
throw new Error("Invalid signature");

// Decode signature — try hex first (SEP-53 / signMessage), then base64 (raw)
const sigBuffer = /^[0-9a-f]+$/i.test(signature)
? Buffer.from(signature, "hex")
: Buffer.from(signature, "base64");

// Try SEP-53 format first: sign(SHA256("Stellar Signed Message:\n" + message))
const sep53Prefix = "Stellar Signed Message:\n";
const prefixedMessage = Buffer.concat([
Buffer.from(sep53Prefix, "utf-8"),
Buffer.from(nonce, "utf-8"),
]);
const messageHash = Buffer.from(
await crypto.subtle.digest("SHA-256", prefixedMessage),
);

if (keypair.verify(messageHash, sigBuffer)) {
span.addEvent("signature_verified_sep53");
} else {
// Fall back to raw signature over nonce bytes
const nonceBuffer = Buffer.from(nonce, "base64");
if (!keypair.verify(nonceBuffer, sigBuffer)) {
throw new Error("Invalid signature");
}
span.addEvent("signature_verified_raw");
}
} catch (e) {
throw e instanceof Error && e.message === "Invalid signature"
Expand Down
Loading