3.9 KiB
server-finish
Purpose
Complete the SRP-6a handshake on the server side after receiving the client's public value A and proof M1. Validates A mod N != 0 (RFC 5054 §2.5.4 abort condition), derives the server-side shared secret S = (A * v^u)^b mod N, hashes it into the session key K = H(S), recomputes the expected M1, and constant-time-compares against the client's M1. On match emits M2 = H(A | M1 | K) per RFC 2945 §3. On mismatch throws SrpError(SRP.AUTH_FAILED).
Features
A mod N == 0gate per RFC 5054 §2.5.4.u ≡ 0 mod Ngate inherited fromcomputeU.- Constant-time
M1comparison via@suaveplan/crypto-utils.constantTimeEqual. - Returns
K,M2, andSso consumers can chain into a KDF onSdirectly. - Emits
srp.handshake_duration_ms{role=server,phase=finish}plus the appropriate outcome counter (ok/auth_failed/public_key_invalid).
Usage: Basic Example
import { serverFinishHandshake } from "@suaveplan/srp/server";
import { params2048Sha256 } from "@suaveplan/srp/params";
import { nobleHashBridge } from "@suaveplan/srp/hash";
const params = params2048Sha256(nobleHashBridge("SHA256"));
const salt = new Uint8Array(16);
const A = new Uint8Array(256);
A[255] = 1;
const B = new Uint8Array(256);
B[255] = 2;
const b = new Uint8Array(32);
b[31] = 1;
const v = new Uint8Array(256);
v[255] = 1;
const M1Client = new Uint8Array(32); // server expects to recompute this and compare
try {
serverFinishHandshake("alice", salt, A, B, b, v, M1Client, params);
} catch (err) {
console.log("auth failed:", (err as Error).message);
}
Usage: Advanced Example
import { serverFinishHandshake, serverStartHandshake } from "@suaveplan/srp/server";
import { clientFinishHandshake, clientStartHandshake } from "@suaveplan/srp/client";
import { computeVerifier } from "@suaveplan/srp/verifier";
import { params2048Sha256 } from "@suaveplan/srp/params";
import { nobleHashBridge } from "@suaveplan/srp/hash";
const params = params2048Sha256(nobleHashBridge("SHA256"));
const salt = new Uint8Array(16);
const rng = {
nextUint32: () => 0,
nextFloat: () => 0,
nextBytes: (n: number) => crypto.getRandomValues(new Uint8Array(n)),
};
const { v } = computeVerifier("alice", "password123", salt, params);
const { a, A } = clientStartHandshake(rng, params);
const { b, B } = serverStartHandshake(v, rng, params);
const client = clientFinishHandshake("alice", "password123", salt, A, B, a, params);
const server = serverFinishHandshake("alice", salt, A, B, b, v, client.M1, params);
console.log(Buffer.from(client.K).equals(Buffer.from(server.K))); // true
API Reference
serverFinishHandshake(I, salt, A, B, b, v, M1Client, params): { K, M2, S }
Throws SrpError(SRP.PUBLIC_KEY_INVALID) when A mod N == 0 or u ≡ 0 mod N. Throws SrpError(SRP.AUTH_FAILED) when the recomputed M1 does not constant-time-match M1Client. On success returns the session key K, the server proof M2, and the raw shared secret S.
Implementation Notes
The order validate A mod N → compute u → compute S → compute K → compute expectedM1 → compare M1Client is intentional: the cheap modular check (A mod N == 0) happens first so an adversary cannot force the server to spend a modPow on garbage; computeU runs next because it has its own abort gate (u ≡ 0); only then do we pay the full modPow chain. This ordering minimises wasted server CPU on adversarial inputs without leaking any new timing channel (every legitimate handshake takes the same path).
The constant-time M1 compare is the last gate before emitting M2. We use expectedM1 (the server's locally-computed proof) inside computeM2 rather than M1Client — even though they are byte-identical when the compare succeeds — because reusing the server-side computation eliminates a class of subtle bugs where a malicious client substitutes a near-miss M1 that happens to match by the time the compare runs.