Skip to content

fix(mpp/ts): enforce 32-byte HMAC secret floor at the @solana/mpp boundary (audit #24)#170

Closed
EfeDurmaz16 wants to merge 1 commit into
solana-foundation:fix/cross-language-auditfrom
EfeDurmaz16:fix/mpp-secret-key-floor
Closed

fix(mpp/ts): enforce 32-byte HMAC secret floor at the @solana/mpp boundary (audit #24)#170
EfeDurmaz16 wants to merge 1 commit into
solana-foundation:fix/cross-language-auditfrom
EfeDurmaz16:fix/mpp-secret-key-floor

Conversation

@EfeDurmaz16

Copy link
Copy Markdown
Collaborator

What

Follow-up to #169. The cross-language audit routed the mppx-owned findings (#1/#24/#15/#9) to an upstream report rather than fixing them in-repo. This closes #24 (weak HMAC secret key) at pay-kit's own boundary as defense-in-depth, without forking or vendoring mppx.

mppx@0.5.x accepts any non-empty secretKey as the HMAC-SHA256 key that binds challenge IDs, so a weak key (e.g. "key") lets an attacker forge challenges. Per NIST SP 800-107 the HMAC-SHA256 key should be at least the hash output size (32 bytes).

How

server/secret-guard.ts wraps Mppx.create and rejects a resolved secret shorter than 32 bytes, checking both the explicit secretKey and the MPP_SECRET_KEY env path. When no secret is set at all it stays silent and defers to mppx's own error, so it owns only the strength check. This mirrors the existing challenge-guard.ts boundary pattern and touches only the secret-strength gate, never signing or settlement.

server/index.ts now re-exports the guarded Mppx so import { Mppx } from '@solana/mpp/server' gets the floor automatically.

Notes

Testing

  • pnpm typecheck, pnpm lint, pnpm format:check clean
  • Full TS suite: 467 passed (4 new guard tests covering explicit/env, short/at-floor)
  • The full-suite run surfaced that pay-kit's adapter production path now enforces the floor too (intended)

Based on the #169 branch; will retarget to main once #169 merges.

…olana-foundation#24)

mppx@0.5.x accepts any non-empty secretKey as the HMAC-SHA256 key that
binds challenge IDs, so a weak key (e.g. "key") lets an attacker forge
challenges. Until a floor lands upstream in mppx, gate at the @solana/mpp
boundary: wrap Mppx.create to reject a secret shorter than 32 bytes
(NIST SP 800-107), checking both the explicit secretKey and the
MPP_SECRET_KEY env path. Mirrors the existing challenge-guard pattern; the
missing-secret case still defers to mppx's own error.

Bumps test secrets that were under the new floor.
@greptile-apps

greptile-apps Bot commented Jun 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a defense-in-depth HMAC secret-strength guard at the @solana/mpp boundary, wrapping mppx's Mppx.create to reject secrets shorter than 32 bytes (NIST SP 800-107 floor) before any challenge is signed, and bumps all existing test secrets that fell below that floor.

  • server/secret-guard.ts introduces a thin Mppx wrapper that checks the byte length of secretKey (or MPP_SECRET_KEY env var) via TextEncoder and re-exports the result as the package's public Mppx; server/index.ts is updated to surface this guarded namespace instead of the raw mppx/server one.
  • Four new unit tests cover the explicit-key and env-var paths at and below the floor; six test secrets across integration and adapter test files are padded to ≥32 bytes.

Confidence Score: 3/5

The guard logic and test-secret bumps are sound, but the spread-based delegation in secret-guard.ts may silently hollow out the guarded Mppx object if mppx exposes its namespace as a class with non-enumerable statics, which warrants verification before merging.

The core concern is the spread pattern in secret-guard.ts: if mppx ships Mppx as a class, only create survives the spread and any other static methods would be undefined at runtime while TypeScript types still declare them. The missing tests for no-secret pass-through and conflicting-signal priority are secondary gaps. All other files are low-risk mechanical bumps.

typescript/packages/mpp/src/server/secret-guard.ts warrants a close look at how MppxBase is structured upstream before merging; typescript/packages/mpp/src/tests/secret-guard.test.ts would benefit from two additional test cases.

Important Files Changed

Filename Overview
typescript/packages/mpp/src/server/secret-guard.ts New guard that wraps Mppx.create to enforce a 32-byte HMAC secret floor; spread-based delegation { ...MppxBase, create } may silently drop non-enumerable mppx static methods if the upstream uses class syntax.
typescript/packages/mpp/src/tests/secret-guard.test.ts Four new guard unit tests covering explicit/env paths at and below the floor; missing cases for no-secret pass-through and conflicting secretKey+MPP_SECRET_KEY priority.
typescript/packages/mpp/src/server/index.ts Re-routes the Mppx public export from mppx/server to the local secret-guard wrapper; clean change that makes the guard transparent to downstream consumers.
typescript/packages/mpp/src/tests/cross-route-replay.test.ts Test secret bumped to 39 bytes to meet the new floor; still imports unguarded Mppx from mppx/server directly, and the comment is misleading about guard enforcement.
typescript/packages/mpp/src/tests/integration.test.ts Four integration test secrets padded to ≥32 bytes; all replacements are correct and logically equivalent.
typescript/packages/pay-kit/src/tests/mpp-adapter.test.ts Adapter test secret padded to 33 bytes (≥32); straightforward update with no logic changes.

Comments Outside Diff (1)

  1. typescript/packages/mpp/src/__tests__/cross-route-replay.test.ts, line 18-23 (link)

    P2 Misleading guard comment — import bypasses the secret-guard

    The comment // >= 32 bytes (audit #24 secret-key floor enforced by the secret guard) implies the guard is active here, but the import on line 18 is import { Mppx } from 'mppx/server' — the unguarded upstream Mppx, not @solana/mpp/server's wrapped version. The 32-byte constant is correct, but a reader could wrongly assume this test validates guard behaviour. The comment should be phrased as "updated to meet the audit Compatibility with another mpp #24 floor" rather than implying enforcement.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "fix(mpp/ts): enforce 32-byte HMAC secret..." | Re-trigger Greptile

Comment on lines +59 to +62
export const Mppx: typeof MppxBase = {
...MppxBase,
create,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Spread may silently drop non-enumerable methods from mppx

{ ...MppxBase, create } only copies own enumerable properties from MppxBase. If mppx exposes Mppx as a class (where static methods have enumerable: false by spec), every method except the explicitly overridden create will be silently absent from the guarded object at runtime even though TypeScript's typeof MppxBase type says they exist. Current tests may all exercise create-only paths, masking this. A safer alternative is to use Object.create or an explicit Proxy so future mppx additions are automatically delegated.

Comment on lines +17 to +38
afterEach(() => {
delete process.env.MPP_SECRET_KEY;
});

test('rejects an explicit secret key shorter than the floor', () => {
expect(() => Mppx.create({ methods: methods(), secretKey: tooShort })).toThrow(/at least 32 bytes/);
});

test('accepts an explicit secret key at the floor', () => {
delete process.env.MPP_SECRET_KEY;
expect(() => Mppx.create({ methods: methods(), secretKey: strong })).not.toThrow();
});

test('rejects a short MPP_SECRET_KEY from the environment', () => {
process.env.MPP_SECRET_KEY = tooShort;
expect(() => Mppx.create({ methods: methods() })).toThrow(/at least 32 bytes/);
});

test('accepts a strong MPP_SECRET_KEY from the environment', () => {
process.env.MPP_SECRET_KEY = strong;
expect(() => Mppx.create({ methods: methods() })).not.toThrow();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing tests for "no secret" and conflicting-signal cases

Two behaviours documented in the guard but not exercised by the suite: (1) when neither secretKey nor MPP_SECRET_KEY is set the guard must stay silent (currently only an inline comment guarantees this); (2) when both are set and they differ in strength, only secretKey is evaluated due to ?? short-circuiting — a short env var with a strong explicit key should be accepted, and a strong env var with a short explicit key should be rejected. Without these cases the guard's priority semantics are untested and a silent regression would be easy to miss.

@lgalabru lgalabru deleted the branch solana-foundation:fix/cross-language-audit June 17, 2026 20:33
@lgalabru lgalabru closed this Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants