Files
skills/docs/security-stack/security/srp/src/math/proof.md
2026-05-29 08:20:56 +01:00

3.0 KiB

proof

Purpose

Internal helpers for computing the SRP-6a / RFC 2945 §3 mutual-authentication proofs M1 and M2. Both client and server compute these values: the client emits M1 = H(H(N) XOR H(g) | H(I) | salt | A | B | K) to prove it knows the password, and the server emits M2 = H(A | M1 | K) to prove it knows the verifier. Co-locating the math in one module keeps the formula in exactly one place so any future correction (e.g. a hash-binding change) lands consistently across both roles.

Features

  • computeM1(I, salt, A, B, K, params) — RFC 2945 §3 client proof.
  • computeM2(A, M1, K, params) — RFC 2945 §3 server proof.
  • Equal-length XOR helper for the H(N) XOR H(g) term — bound to the chosen params.hash digest size.
  • UTF-8 encode I once per call.

Usage: Basic Example

import { computeM1, computeM2 } from "@suaveplan/srp";
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);
const B = new Uint8Array(256);
const K = new Uint8Array(32);
const M1 = computeM1("alice", salt, A, B, K, params);
const M2 = computeM2(A, M1, K, params);
console.log(M1.byteLength, M2.byteLength); // 32 32

Usage: Advanced Example

import { computeM1, computeM2 } from "@suaveplan/srp";
import { params2048Sha256 } from "@suaveplan/srp/params";
import { nobleHashBridge } from "@suaveplan/srp/hash";
import { constantTimeEqual } from "@suaveplan/crypto-utils";

const params = params2048Sha256(nobleHashBridge("SHA256"));
const salt = new Uint8Array(16);
const A = new Uint8Array(256);
const B = new Uint8Array(256);
const K = new Uint8Array(32);

const clientM1 = computeM1("alice", salt, A, B, K, params);
const serverExpectedM1 = computeM1("alice", salt, A, B, K, params);
console.log(constantTimeEqual(clientM1, serverExpectedM1));

API Reference

computeM1(I, salt, A, B, K, params): Uint8Array

Returns the client proof H(H(N) XOR H(g) | H(I) | salt | A | B | K). Result length equals params.hash.digestSize.

computeM2(A, M1, K, params): Uint8Array

Returns the server proof H(A | M1 | K). Result length equals params.hash.digestSize.

Implementation Notes

The H(N) XOR H(g) term is a deliberate design choice in RFC 2945 — it commits both ends to the chosen group AND ties the proof to the hash function. An attacker who substituted a weakened group during transport would produce a different H(N) XOR H(g) value than the legitimate counterpart computed, causing the proof to fail.

computeM1 deliberately accepts A and B as-is rather than re-padding them. RFC 2945's formula is over the un-padded public values, AND the values supplied to computeM1 from the handshake functions are already produced by bigIntToBytes(_, params.N.byteLength) and so are already zero-padded to the modulus width. Double-padding would produce off-by-N-byte digests.