Skip to content

feat(tsp-js): pure-TS HPKE on @noble — one implementation for browser, Node, and React Native - #116

Merged
stormer78 merged 3 commits into
OpenVTC:mainfrom
albertoleon7794:noble-hpke
Aug 17, 2026
Merged

feat(tsp-js): pure-TS HPKE on @noble — one implementation for browser, Node, and React Native#116
stormer78 merged 3 commits into
OpenVTC:mainfrom
albertoleon7794:noble-hpke

Conversation

@albertoleon7794

@albertoleon7794 albertoleon7794 commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

hpke-js reaches for crypto.subtle for HKDF and X25519. React Native's Hermes engine has none, so @openvtc/vti-tsp-js cannot run in a React Native wallet today — and real apps polyfill subtle only partially (a wallet that provides subtle.digest alone looks WebCrypto-capable to any feature probe, then fails at runtime).

This replaces the hpke-js path with an RFC 9180 HPKE-Auth implementation built on the @noble primitives (curves/hashes/ciphers) — one code path, every runtime, per our sync: no environment detection, no per-environment behavior. Same suite (KEM 0x0020, KDF 0x0001, AEAD 0x0003, mode_auth), single-shot, byte-identical output. seal/open signatures and wire bytes are unchanged; the only runtime requirement is crypto.getRandomValues (native in browsers/Node; react-native-get-random-values on RN).

hpke-js stays as a dev-dependency: an equivalence test holds the shipped implementation byte-identical to it on every CI run.

Verified — 48 tests pass (re-run on this branch rebased to current main):

  • the package's existing 42 tests against the noble implementation, including tests/interop.rust-vector.mjs (the fixed-key affinidi-tsp vector)
  • the official CFRG RFC 9180 mode_auth vector asserted in-tree (tests/crypto.cfrg-vector.mjs): AuthEncap enc + shared_secret, AuthDecap, seal reproducing the vector ciphertext, open recovering its plaintext — every key fixed, so exactly one correct output
  • cross-implementation equivalence (tests/crypto.hpke-js-equivalence.mjs): each implementation opens the other's output

Verified across runtimes (probes in the Keyring reference suite):

  • Node 20 and React Native's Hermes VM, same bundled file: 9/9 checks pass on both, byte-identical transcripts — ref-03b
  • inside a real React Native wallet app (Xcode build, Metro bundle, iOS simulator, production polyfills loaded): same 9/9, same transcript hash — ref-03c

Why not an off-the-shelf noble HPKE (e.g. @panva/hpke-noble)? TSP needs Auth mode (mode_auth, 0x02), which the mainstream WebCrypto-free options didn't cover when checked — and the composed recipe is ~140 lines on the same @noble primitives, small enough that owning it inside the ecosystem beats taking an external dependency for it.

Reviewed and merged on our fork first (albertoleon7794#1); cherry-picked here onto current main (no overlap with the intervening commits — none touched packages/tsp-js).

Scope of the "runs on React Native" claim, after the consolidation commit (b086285): complete for @openvtc/vti-tsp-js (zero WebCrypto anywhere). For @openvtc/pnm-core, this removes HPKE from its crypto.subtle dependencies — WebAuthn, the PRF vault wrap, DID verification methods and trust-task canonicalisation still use subtle, so core's RN story is closer, not closed.

TL;DR: swaps the HPKE internals from WebCrypto/hpke-js to a pure-TS @noble implementation — same API, same wire bytes, and vti-tsp-js now runs on React Native (and any partial-WebCrypto runtime). hpke-js stays as a dev-dependency proving byte-equivalence in CI. After the consolidation commit: one implementation serves both HPKE modes and both packages, @hpke/* is out of both production dependency trees (51/51 + 215/215 tests), and the RN claim is complete for tsp-js, partial for core. Byte-identical across Node, the Hermes VM, and a real RN app; official RFC 9180 vector asserted in-tree.

hpke-js reaches for crypto.subtle for HKDF and X25519. React Native's
Hermes engine has none, so @openvtc/vti-tsp-js cannot run in a React
Native wallet today; some older Node and edge runtimes ship subtle only
partially. This adds an RFC 9180 HPKE-Auth implementation built on the
@noble primitives (curves/hashes/ciphers) and selects it automatically
when crypto.subtle is unavailable, keeping the package's 'runs anywhere'
promise literal.

Same suite (KEM 0x0020, KDF 0x0001, AEAD 0x0003, mode_auth), single-shot,
byte-identical output. seal/open signatures are unchanged and no public
API is removed; TSP_HPKE_BACKEND=noble|webcrypto forces a backend so the
suite can be run against both.

Verified:
- the package's own 42 tests pass on both backends, including
  tests/interop.rust-vector.mjs (the fixed-key affinidi-tsp vector)
- the official CFRG mode_auth vector for this suite is reproduced
  byte-exact (AuthEncap enc + shared_secret, AuthDecap, sealed ciphertext)
- cross-implementation: each backend opens the other's output

Signed-off-by: Alberto L <alberto_leon@seas.harvard.edu>
… everywhere

Per maintainer review: one code path in every runtime, no environment
sniffing. Real apps make the sniff unreliable anyway — a wallet that
polyfills only crypto.subtle.digest looks WebCrypto-capable to a feature
probe and fails at runtime; with a single implementation that failure
class is deleted rather than defended against.

hpke-js moves to a dev-dependency, kept for the equivalence test that
holds the shipped implementation byte-identical to an independent
RFC 9180 implementation on every CI run. The official CFRG mode_auth
vector for the suite is asserted in-tree as well (every key fixed,
including the ephemeral, so there is exactly one correct output).

Public API is unchanged from upstream: same seal/open signatures, same
wire bytes. The detection-era additions (activeBackend/setBackend/
TSP_HPKE_BACKEND) introduced earlier on this branch are gone. The only
runtime requirement is crypto.getRandomValues — native in browsers and
Node, react-native-get-random-values on React Native.

Tests: 48 pass (42 existing + 4 CFRG-vector + 2 hpke-js equivalence),
including the fixed-key Rust affinidi-tsp interop vector.

Signed-off-by: Alberto L <alberto_leon@seas.harvard.edu>
@albertoleon7794
albertoleon7794 marked this pull request as ready for review August 16, 2026 16:40
stormer78 added a commit to albertoleon7794/vta-browser-plugin that referenced this pull request Aug 16, 2026
Follows up the noble-hpke swap with the other half of the job: `pnm-core`
still carried its own HPKE, so the repo had two independent RFC 9180 key
schedules (base mode in core, auth mode in tsp-js) and `@hpke/*` was still a
*production* dependency of the package a React Native wallet installs.

- `hpke-noble.ts` now owns both modes. Base and auth already shared the KEM,
  the key schedule and the AEAD; they differ only in the DH inputs to
  Encap/Decap and the mode byte, so they are one file with `mode` as a
  parameter rather than two copies (stack guide R4.1).
- `@openvtc/vti-tsp-js/hpke` is a new subpath export. `pnm-core`'s
  sealed-bundle path imports `openBase` from it and drops `@hpke/core` +
  `@hpke/chacha20poly1305` from its runtime deps. `hpkeOpen` keeps its exact
  signature, validation and error strings.
- hpke-js stays a dev-dependency in both packages and now pins *both* modes
  byte-identical on every CI run, each implementation opening the other's
  output.

`hpkeOpen` had no direct test coverage, so this adds
`provision.hpke-open.mjs` — seven cases sealed by hpke-js (deliberately the
other implementation; a round-trip against our own seal would pass even if
both drifted) covering the pinned `vta-sealed-transfer/v1` info binding, the
chunk-header AAD binding, wrong-recipient, tampering, and length validation.

Also from review of OpenVTC#116:
- the tsp-js README and package description still advertised "WebCrypto /
  hpke-js" and "runs anywhere WebCrypto does" — the exact claim this branch
  inverts, on the page npm shows.
- the ephemeral-key vector hook is now `__unsafeFixedEphemeralSk` on an
  options object, with the nonce-reuse consequence written down.
- `x25519.utils` is no longer cast, so a noble rename fails the build instead
  of throwing at first seal. The `randomPrivateKey` fallback was already dead
  on the declared range.
- regenerating the lockfile with the repo's npm drops 25 spurious
  `"peer": true` entries.

Verified: lint, build, 51/51 tsp-js, 215/215 core, MV3 bundle still a single
chunk with no dynamic import and no hpke-js.

Note: `pnm-core` still uses `crypto.subtle` for WebAuthn, the PRF vault wrap,
DID verification methods and trust-task canonicalisation, so it is not yet
subtle-free on React Native — this removes HPKE from that list, not the rest.
@stormer78

Copy link
Copy Markdown
Contributor

Reviewed this properly — checked the branch out and audited the RFC 9180 math line by line against §4, §4.1, §5.1 and §5.1.4: LabeledExtract/LabeledExpand argument order against noble's extract/expand, both suite IDs, the mode_auth key-schedule context, the AuthEncap/AuthDecap dh and kem_context ordering, and seq-0 nonce handling. All correct, and the in-tree CFRG vector pins it. I also probed a few things the tests don't: seal/open don't mutate caller buffers, unaligned subarray inputs round-trip, malformed inputs throw cleanly rather than returning garbage, and @noble/ciphers@2.0.1 — the floor of the declared ^2.0.0 — does export ./chacha.js. No correctness bug. Nice piece of work, and the three-way pinning (CFRG vector + cross-implementation equivalence) is the right way to ship hand-rolled crypto.

Heads-up that I've pushed a commit to this branch (e1251e9) rather than leaving a pile of review comments — apologies for not flagging it first. Happy to reshape or drop any of it; details below so nothing is a surprise.

Finishing the job in pnm-core

The part that made me push rather than comment: packages/core still had its own HPKE and still listed @hpke/core + @hpke/chacha20poly1305 as production dependencies. So @openvtc/pnm-core — the package a React Native wallet actually installs — still pulled in a crypto.subtle dependency, and the repo carried two independent RFC 9180 key schedules (base mode in core, auth mode here). That's the R4.1 duplication the stack guide warns about, and it meant the "runs on React Native" outcome stopped at the tsp-js boundary.

Base and auth already shared the KEM, the key schedule and the AEAD — they differ only in the DH inputs to Encap/Decap and the mode byte — so hpke-noble.ts now takes mode as a parameter instead of hosting a second copy. @openvtc/vti-tsp-js/hpke is a new subpath export; core's sealed-bundle path imports openBase from it and drops both @hpke/* runtime deps. hpkeOpen keeps its exact signature, validation and error strings.

hpkeOpen had no test coverage

Worth calling out on its own: core's 208 tests never exercised hpkeOpen, so rewriting that decryption path was initially unverified. Added provision.hpke-open.mjs — seven cases where the sealing side is hpke-js, deliberately the other implementation, since a round-trip against our own seal would pass even if both directions drifted together. Covers the pinned vta-sealed-transfer/v1 info binding, the chunk-header AAD binding, wrong-recipient, tampering, and length validation.

hpke-js stays a dev-dependency in both packages and now pins both modes byte-identical on every CI run.

Smaller things from the review

  • The README and package.json description still said "WebCrypto / hpke-js" and "Runs anywhere WebCrypto does" — the exact claim this branch inverts, on the page npm shows. Rewritten.
  • The ephemeral-key vector hook is now __unsafeFixedEphemeralSk on an options object, with the nonce-reuse consequence written down next to it. It's reachable only in-repo via the exports map, but nothing stopped a second call reusing (key, base_nonce).
  • Dropped the x25519.utils cast, so a noble rename fails the build instead of throwing TypeError at first seal in someone's wallet. The randomPrivateKey fallback was already dead on the declared range.
  • Regenerating the lockfile with the repo's npm (11.19.0) drops 25 spurious "peer": true entries. The libc fields your npm stripped are not restored — npm won't re-add them without a full metadata refetch, which reintroduces exactly the churn we're trying to remove. Left as-is deliberately.

Verified

Lint clean across all four workspaces, build clean, 51/51 tsp-js (was 48) and 215/215 core (was 208), MV3 bundle still a single chunk with no dynamic import() and no hpke-js. npm ls --omit=dev confirms @hpke/* is gone from both production trees.

Two caveats

  • pnm-core is not subtle-free. It still uses crypto.subtle for WebAuthn, the PRF vault wrap, DID verification methods and trust-task canonicalisation. This removes HPKE from that list, not the rest — so the RN story is complete for tsp-js and only closer for core. Worth softening that framing in the PR description before merge.
  • I tried to A/B the background bundle size before and after, but tsc -b is incremental and the comparison build almost certainly still linked the new compiled core/dist, so I'm not claiming a size number. The verified win is the dependency tree, not bundle bytes.

Answering the "why not an off-the-shelf noble HPKE" point pre-emptively since it came up: agreed on the reasoning, and the consolidation strengthens it — one ~200-line file now serves both modes and both packages, which is a better ratio than taking an external dependency for auth mode alone.

Follows up the noble-hpke swap with the other half of the job: `pnm-core`
still carried its own HPKE, so the repo had two independent RFC 9180 key
schedules (base mode in core, auth mode in tsp-js) and `@hpke/*` was still a
*production* dependency of the package a React Native wallet installs.

- `hpke-noble.ts` now owns both modes. Base and auth already shared the KEM,
  the key schedule and the AEAD; they differ only in the DH inputs to
  Encap/Decap and the mode byte, so they are one file with `mode` as a
  parameter rather than two copies (stack guide R4.1).
- `@openvtc/vti-tsp-js/hpke` is a new subpath export. `pnm-core`'s
  sealed-bundle path imports `openBase` from it and drops `@hpke/core` +
  `@hpke/chacha20poly1305` from its runtime deps. `hpkeOpen` keeps its exact
  signature, validation and error strings.
- hpke-js stays a dev-dependency in both packages and now pins *both* modes
  byte-identical on every CI run, each implementation opening the other's
  output.

`hpkeOpen` had no direct test coverage, so this adds
`provision.hpke-open.mjs` — seven cases sealed by hpke-js (deliberately the
other implementation; a round-trip against our own seal would pass even if
both drifted) covering the pinned `vta-sealed-transfer/v1` info binding, the
chunk-header AAD binding, wrong-recipient, tampering, and length validation.

Also from review of OpenVTC#116:
- the tsp-js README and package description still advertised "WebCrypto /
  hpke-js" and "runs anywhere WebCrypto does" — the exact claim this branch
  inverts, on the page npm shows.
- the ephemeral-key vector hook is now `__unsafeFixedEphemeralSk` on an
  options object, with the nonce-reuse consequence written down.
- `x25519.utils` is no longer cast, so a noble rename fails the build instead
  of throwing at first seal. The `randomPrivateKey` fallback was already dead
  on the declared range.
- regenerating the lockfile with the repo's npm drops 25 spurious
  `"peer": true` entries.

Verified: lint, build, 51/51 tsp-js, 215/215 core, MV3 bundle still a single
chunk with no dynamic import and no hpke-js.

Note: `pnm-core` still uses `crypto.subtle` for WebAuthn, the PRF vault wrap,
DID verification methods and trust-task canonicalisation, so it is not yet
subtle-free on React Native — this removes HPKE from that list, not the rest.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@albertoleon7794

Copy link
Copy Markdown
Contributor Author

Pulled b086285 and reproduced locally from a clean npm ci — 51/51 tsp-js, 215/215 core. The consolidation is the right call; thanks for finishing the job into core rather than leaving it as review comments. Description updated to scope the RN claim as you suggested: complete for tsp-js, closer-not-closed for core. Flipping to ready.

@affinidi-appsecurity-bot

Copy link
Copy Markdown

🛡️ AI Agentic Security Review

⚠️ Security Report — 0 confirmed issues

PR #116vta-browser-plugin • Review the attached reports for details and recommended actions.


🤖 AI-Generated — This review validates findings against source code.
Remediation suggestions should be tested before applying. Engineers own the final implementation.
When in doubt, consult the Security team.


🎯 Scope: changes only. This review covers only the code introduced by this MR/PR's diff, so a clean result means "no new issues" — not "no issues at all." Whole-codebase coverage is handled by the scheduled repository scans.

📊 Summary

Severity Issues
Total Confirmed 0
⚠️ Must-Review-By-Human 2
False Positives 2 (removed)

⚠️ 2 finding(s) need human review — the automated validation was inconclusive (insufficient evidence). These are not dismissed; please have a developer / the Security team read and decide.


⚠️ Must-Review-By-Human (2) — click to collapse
  • 🟠 Hand-rolled RFC 9180 HPKE re-implementation replaces audited library — correctness/regression risk — EVIDENCE FOUND: hpke-noble.ts is explicitly named in the finding and its content is described as replacing @hpke/core+@hpke/chacha20poly1305 with a from-scratch RFC 9180 implementation ('// HPKE (RFC 9180) implemented on @noble primitives —…
  • 🟡 Unsafe ephemeral-key override function shipped in production crypto module (nonce/key reuse escape hatch) — EVIDENCE FOUND: The finding quotes 'type UnsafeFixedEphemeral = { __unsafeFixedEphemeralSk?: Uint8Array }; export function encap(recipientPk, unsafe?) { const skE = unsafe?.__unsafeFixedEphemeralSk ??

These were validated up to a point but need a human to make the final call.


📎 Reports

🔒 Security Validation Report (mandatory review — confirmed, materialised security issues)

📄 Open full Security Validation Report — validation_report_PR116_2026-08-16T19-29-00.md

🛡️ Security Validation Report — PR #116

Field Value
Repository OpenVTC/vta-browser-plugin
Branch noble-hpkemain
Validated 2026-08-16
Scan ID 8c658f0d
Validator AI Security Validation Agent

🗺️ Scan Coverage

Modules scanned: 3 · with findings: 2 · files: 12 · findings: 4

Module Files scanned Findings
packages/tsp-js 8 2
packages/core 3 2
(root) 1 0

Executive Summary

Category Confirmed Must-Review-By-Human False Positive Duplicate Not Applicable Total
Security Issues 0 2 2 0 0 4

⚠️ 2 finding(s) need human review. These could not be conclusively confirmed or dismissed automatically (insufficient evidence). They are not dismissed — a developer / security team member must read and decide.


🔒 Security Issues

⚠️ Must-Review-By-Human (2)

Validated up to a point, but inconclusive — a human must read the code and make the final call. Reported (not dismissed) so developers and the security team receive them.

🟠 Hand-rolled RFC 9180 HPKE re-implementation replaces audited library — correctness/regression risk

Field Detail
Severity HIGH
Location packages/tsp-js/src/crypto/hpke-noble.ts:1
Finding ID github_pr-496087bc4c21
CWE CWE-327, CWE-1240
OWASP A02:2021 - Cryptographic Failures
MITRE ATT&CK T1600 - Weaken Encryption
CAPEC CAPEC-97
CVSS 4.0 6.9 (CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N)
DREAD 5.2
Reachability 🔴 Reachable
Exploit Maturity theoretical
Detection Source skill_scan

📝 Description:

A correctness flaw in this module would compromise confidentiality and/or integrity of every TSP message and every VTA sealed-bundle transfer processed by any consumer of @openvtc/vti-tsp-js, including the browser extension and wallet applications — this is the sole cryptographic engine for the entire ecosystem's secure transfer protocol.

🧪 Proof of Concept:

This is the core RFC 9180 KeySchedule function, hand-implemented from the specification. Any subtle error here (wrong ordering of ksContext concatenation, incorrect label bytes, off-by-one in length encoding) would produce a key/nonce that appears internally consistent but diverges from the standard — potentially only detectable via differential testing against another implementation or formal verification, not via unit tests that only check self-consistency.

function keySchedule(
  mode: number,
  sharedSecret: Uint8Array,
  info: Uint8Array,
): { key: Uint8Array; baseNonce: Uint8Array } {
  const pskIdHash = labeledExtract(HPKE_SUITE_ID, EMPTY, "psk_id_hash", EMPTY);
  const infoHash = labeledExtract(HPKE_SUITE_ID, EMPTY, "info_hash", info);
  const ksContext = cat(new Uint8Array([mode]), pskIdHash, infoHash);
  const secret = labeledExtract(HPKE_SUITE_ID, sharedSecret, "secret", EMPTY);
  return {
    key: labeledExpand(HPKE_SUITE_ID, secret, "key", ksContext, NK),
    baseNonce: labeledExpand(HPKE_SUITE_ID, secret, "base_nonce", ksContext, NN),
  };
}

Vulnerable lines: 76, 100

🔎 Evidence: packages/tsp-js/src/crypto/hpke-noble.ts:1

// HPKE (RFC 9180) implemented on @noble primitives — no WebCrypto.
function keySchedule(mode: number, sharedSecret: Uint8Array, info: Uint8Array) { ... }
function extractAndExpand(dhBytes: Uint8Array, kemContext: Uint8Array): Uint8Array { ... }

💥 Impact:

A correctness flaw in this module would compromise confidentiality and/or integrity of every TSP message and every VTA sealed-bundle transfer processed by any consumer of @openvtc/vti-tsp-js, including the browser extension and wallet applications — this is the sole cryptographic engine for the entire ecosystem's secure transfer protocol.

Confidentiality: High — any key-schedule/KEM flaw could allow plaintext recovery of wallet transfer data · Integrity: High — could allow forged/tampered messages to be accepted · Availability: Low — a strict/incorrect implementation could also cause valid messages to fail (functional regression, not security per se)

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: none
  • Attack path: EP-001/EP-002 (hpke.seal/open, TSP messages) and EP-003/EP-004 (hpke.sealBase/openBase) and EP-005 (hpkeOpen in packages/core, VTA sealed bundle decryption) → noble.seal/open/sealBase/openBase in hpke-noble.ts → keySchedule()/extractAndExpand()/dh() at lines ~65-100

⚖️ Triage Factors:

Factor Value
Fixable ✅ Yes
Exploitability low
Business impact high
Public exploit None known
Environment unknown

Attack scenario: The production HPKE cryptographic engine was rewritten from scratch to remove a WebCrypto dependency, replacing an audited library with code validated only by one fixed test vector and equivalence tests authored by the same implementer, creating correctness/regression risk across the entire ecosystem's secure transfer protocol.

🔧 Remediation:

⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.

Rather than a code diff, the primary remediation is process-based: commission an independent cryptographic audit of hpke-noble.ts, add differential fuzz testing (e.g., fast-check-based property tests comparing outputs against hpke-js/@hpke across thousands of random inputs, not just one fixed vector plus a handful of random equivalence tests), and add differential testing against the Rust affinidi-tsp reference. Until that audit completes, consider keeping the audited library as the default production path.

Vulnerable code:

// keySchedule, extractAndExpand, dh — hand-rolled RFC 9180 primitives
function keySchedule(mode, sharedSecret, info) { /* custom implementation */ }

Secure code:

// Recommended interim mitigation: retain @hpke/core + @hpke/chacha20poly1305
// as the production path behind a feature flag until hpke-noble.ts has
// undergone independent cryptographic audit and property-based/differential
// fuzz testing against the Rust reference implementation.
import { CipherSuite, DhkemX25519HkdfSha256, HkdfSha256 } from "@hpke/core";
import { Chacha20Poly1305 } from "@hpke/chacha20poly1305";
// ... use audited suite() as primary; hpke-noble.ts used only where WebCrypto is unavailable, gated by explicit runtime detection with a documented risk acceptance.

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 60%
  • AI Validation Evidence: EVIDENCE FOUND: hpke-noble.ts is explicitly named in the finding and its content is described as replacing @hpke/core+@hpke/chacha20poly1305 with a from-scratch RFC 9180 implementation ('// HPKE (RFC 9180) implemented on @noble primitives — no WebCrypto.'). packages/core/src/provision/hpke.ts (provided in source_files) still shows the OLD implementation using '@hpke/core' and '@hpke/chacha20poly1305' via CipherSuite, DhkemX25519HkdfSha256, HkdfSha256, Chacha20Poly1305 — this is the pre-swap version, meaning the actual hpke-noble.ts file content was not included in source_files for direct verification of the claimed hand-rolled key schedule internals (keySchedule, extractAndExpand functions). EVIDENCE NOT FOUND: The actual full content of hpke-noble.ts (only a short snippet in evidence) is not present in source_files to verify correctness or absence of audited-library fallback; cannot independently confirm the specific RFC9180 conformance claims (CFRG vector tests, hpke-js equivalence tests mentioned) since those test files are also not in source_files. CHANGED VS PRE-EXISTING: CHANGED — packages/tsp-js/src/crypto/hpke-noble.ts is the explicit target file of this finding and is clearly part of this MR's new crypto module (title states 'replaces audited library'), consistent with the MR's stated goal of switching HPKE backend. VERDICT JUSTIFICATION: The architectural risk described (replacing an audited library with hand-rolled crypto) is a legitimate design-level security concern and the file is confirmed changed by this MR, but since the full hpke-noble.ts source is not available to verify implementation correctness/test coverage claims, a human cryptography reviewer must make the final call — this is inherently a judgment call about acceptable risk, not a concrete exploitable bug I can trace end-to-end.
  • Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.

🟡 Unsafe ephemeral-key override function shipped in production crypto module (nonce/key reuse escape hatch)

Field Detail
Severity MEDIUM
Location packages/tsp-js/src/crypto/hpke-noble.ts:95
Finding ID github_pr-c2b61f817804
CWE CWE-323, CWE-489, CWE-1188
OWASP A02:2021 - Cryptographic Failures
MITRE ATT&CK T1600 - Weaken Encryption
CAPEC CAPEC-97, CAPEC-20
CVSS 4.0 7.6 (CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N)
DREAD 6.2
Reachability ⚪ Not reachable
Exploit Maturity conceptual
Detection Source skill_scan

📝 Description:

If this test-only override is ever inadvertently reachable in production (via a future API change, direct internal import, or supply-chain compromise), an attacker could recover plaintext of VTA sealed-transfer bundles or TSP messages and forge subsequent messages, compromising wallet credential/transfer data confidentiality and integrity.

🧪 Proof of Concept:

The exported encap()/authEncap()/seal()/sealBase() functions accept an optional override for the ephemeral secret key, which — while unused by the public hpke.ts wrapper today — is a live code path shipped in the production bundle that, if ever reached with an attacker- or dev-controlled fixed value across multiple seal operations, breaks ChaCha20Poly1305's single-use nonce/key requirement.

type UnsafeFixedEphemeral = { __unsafeFixedEphemeralSk?: Uint8Array };

/** §4.1 Encap (base mode). Exported for test-vector verification. */
export function encap(recipientPk: Uint8Array, unsafe?: UnsafeFixedEphemeral): {
  sharedSecret: Uint8Array;
  enc: Uint8Array;
} {
  const skE = unsafe?.__unsafeFixedEphemeralSk ?? x25519.utils.randomSecretKey();
  const enc = x25519.getPublicKey(skE);
  return {
    sharedSecret: extractAndExpand(dh(skE, recipientPk), cat(enc, recipientPk)),
    enc,
  };
}

Vulnerable lines: 95, 108

🔎 Evidence: packages/tsp-js/src/crypto/hpke-noble.ts:95

type UnsafeFixedEphemeral = { __unsafeFixedEphemeralSk?: Uint8Array };
export function encap(recipientPk: Uint8Array, unsafe?: UnsafeFixedEphemeral) {
  const skE = unsafe?.__unsafeFixedEphemeralSk ?? x25519.utils.randomSecretKey();

💥 Impact:

If this test-only override is ever inadvertently reachable in production (via a future API change, direct internal import, or supply-chain compromise), an attacker could recover plaintext of VTA sealed-transfer bundles or TSP messages and forge subsequent messages, compromising wallet credential/transfer data confidentiality and integrity.

Confidentiality: High if reachable — XOR of plaintexts recoverable under key/nonce reuse · Integrity: High if reachable — Poly1305 one-time key recovery enables forgery · Availability: None

🧭 Reachability:

  • Network exposure: none
  • Auth barrier: none
  • Attack path: hpke-noble.ts exports encap/authEncap/seal/sealBase with unsafe param → NOT forwarded by public hpke.ts wrapper (seal/open/sealBase/openBase in hpke.ts call noble.* without passing unsafe) → not reachable via @openvtc/vti-tsp-js or @openvtc/vti-tsp-js/hpke public exports today

⚖️ Triage Factors:

Factor Value
Fixable ✅ Yes
Exploitability low
Business impact medium
Public exploit None known
Environment unknown

Attack scenario: A test-only ephemeral-key override shipped inside the production crypto module could, if ever reached via a future code path or direct internal import, enable ChaCha20Poly1305 nonce/key reuse and break confidentiality/integrity of sealed messages.

🔧 Remediation:

⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.

Move the CFRG-vector-only fixed-ephemeral capability into a separate test-utility module that is never included in the published package's 'files' allowlist and never imported by hpke.ts. This makes it structurally impossible for the unsafe path to reach production code, rather than relying solely on the public wrapper's discipline not to forward it.

Vulnerable code:

export function encap(recipientPk: Uint8Array, unsafe?: UnsafeFixedEphemeral) {
  const skE = unsafe?.__unsafeFixedEphemeralSk ?? x25519.utils.randomSecretKey();
  ...
}

Secure code:

// hpke-noble.ts — production module: no unsafe override exported.
export function encap(recipientPk: Uint8Array) {
  const skE = x25519.utils.randomSecretKey();
  ...
}

// tests/_internal/hpke-noble-testonly.ts — separate file, excluded from published package.json "files"
export function encapWithFixedEphemeral(recipientPk: Uint8Array, fixedSk: Uint8Array) {
  ... // only imported by test files, never bundled in dist shipped to npm

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 55%
  • AI Validation Evidence: EVIDENCE FOUND: The finding quotes 'type UnsafeFixedEphemeral = { __unsafeFixedEphemeralSk?: Uint8Array }; export function encap(recipientPk, unsafe?) { const skE = unsafe?.__unsafeFixedEphemeralSk ?? x25519.utils.randomSecretKey(); }' directly from hpke-noble.ts, and the affects_summary/security_findings SEC-002 confirms 'The public hpke.ts wrappers deliberately do not forward this parameter' — meaning the unsafe hook exists but is NOT exposed through the public API surface. EVIDENCE NOT FOUND: The full hpke.ts wrapper source and hpke-noble.ts source are not present in source_files to directly verify that seal()/sealBase() in hpke.ts never forward the unsafe param, nor to verify package.json's 'files' array inclusion/exclusion claims. CHANGED VS PRE-EXISTING: CHANGED — hpke-noble.ts is the new file introduced by this MR per the finding's own file_path and the broader PR narrative of introducing this module. VERDICT JUSTIFICATION: This is a real design smell (test-only escape hatch shipped in production module) but exploitability depends on whether the unsafe param is truly unreachable from application code via public exports — this requires human review of the actual dist/package.json exports and hpke.ts wrapper code, which are not fully available here.
  • Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.

❌ False Positives Removed (2)

The following were determined to not be exploitable in this context:

  • Hardcoded cross-language HPKE domain-separation string with no CI-enforced parity check (low, packages/core/src/provision/hpke.ts)

    🔍 Validation Log

    • Verdict: ❌ False Positive
    • Confidence: 85%
    • AI Validation Evidence: EVIDENCE FOUND: packages/core/src/provision/hpke.ts (provided in full) shows 'const HPKE_INFO = new TextEncoder().encode("vta-sealed-transfer/v1");' with a code comment explaining: '// Suite (pinned, matches vta-sdk/src/sealed_transfer/hpke.rs): ... the info string vta-sealed-transfer/v1 domain-separates this suite from any future use of the same primitives.' The HPKE info string is bound into the AEAD key schedule via cs.open({ recipientKey, enc, info: HPKE_INFO }, ciphertext, aad) — meaning any info mismatch would cause AEAD decryption failure (fail-closed), not a silent security bypass. EVIDENCE NOT FOUND: No CI configuration or cross-repo test file confirming automated enforcement between the Rust and JS constants was found in source_files, but the finding itself and the threat model's STRIDE-3 note that domain-separation binding causes AEAD failure on mismatch ('mismatches cause AEAD failure, not silent acceptance') and that a test 'the info string is bound' already exists per the description. CHANGED VS PRE-EXISTING: PRE-EXISTING pattern — this hardcoded constant and its usage exist identically in the provided packages/core/src/provision/hpke.ts file, which itself is only swapping the underlying crypto library backend, not introducing the HPKE_INFO string or its domain-separation design newly. The security_findings entry SEC-005 explicitly marks 'is_new': false for this same issue. VERDICT JUSTIFICATION: This is a low-severity operational/process concern (lack of CI cross-check) rather than an exploitable vulnerability; the AEAD binding fails closed on mismatch as documented, and the finding is explicitly self-labeled as not new by the scan's own security_findings (SEC-005), making this a low-risk config/process gap rather than a validated vulnerability.
    • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.
  • Missing explicit length validation for recipientSecret in hpkeOpen (asymmetric input validation) (low, packages/core/src/provision/hpke.ts)

    🔍 Validation Log

    • Verdict: ❌ False Positive
    • Confidence: 80%
    • AI Validation Evidence: EVIDENCE FOUND: The full source of packages/core/src/provision/hpke.ts is provided and shows: 'if (input.recipientSecret.length !== 32) { throw new Error(hpke: recipientSecret must be 32 bytes (got ${input.recipientSecret.length})); } if (input.kemEncap.length !== 32) { throw new Error(hpke: kemEncap must be 32 bytes (got ${input.kemEncap.length})); }' — contrary to the finding's claim, recipientSecret length IS explicitly validated with the exact same pattern as kemEncap, immediately preceding it in the function. EVIDENCE NOT FOUND: No evidence supporting the claim that recipientSecret validation is missing; the finding's own evidence snippet only shows the kemEncap check because it starts at line 45, but the full file clearly shows the recipientSecret check at the line just above. CHANGED VS PRE-EXISTING: The hpkeOpen function and its validation logic are present in the full provided file, and both checks (recipientSecret and kemEncap) exist together as a unit. VERDICT JUSTIFICATION: The finding is factually incorrect — the deciding code review of the complete function body shows the explicit length check for recipientSecret does exist ('if (input.recipientSecret.length !== 32) { throw new Error(...) }'), directly contradicting the claim of a missing check.
    • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

Generated by Agentic Sec — AI Security Validation Agent
This report includes full scan data + AI validation evidence. Feed to engineering copilots for automated fix deployment.

🛡️ Threat Model & Affect Analysis (supplementary — theoretical threats and MR impact analysis)

🛡️ Open full Threat Model & Affect Analysis — threat-modelling_affect-analysis_report_PR116_2026-08-16T19-29-00.md

🛡️ Threat Model & Affect Analysis — PR #116

Field Value
Repository OpenVTC/vta-browser-plugin
Branch noble-hpkemain
Generated 2026-08-16

ℹ️ This report contains theoretical threats and impact analysis for the MR.
Unlike the Security Validation Report (which contains confirmed, materialised issues),
these are potential risks that may or may not be exploitable. Use this for defence-in-depth planning.


📋 Affect Analysis

Change Summary

Replaces the third-party audited hpke-js (WebCrypto-backed) HPKE implementation used for VTA sealed-bundle decryption and TSP messaging with a single hand-rolled, pure-TypeScript RFC 9180 HPKE implementation on @noble primitives, enabling one code path across browser, Node, and React Native (no WebCrypto/WASM dependency).

Diff: +671 / -121 lines
Types: feature, security, refactor, config, test, docs

🧩 Affected Components

Component Impact Change What Changed
HPKE Cryptography Core (hpke-noble.ts) critical added A complete, from-scratch RFC 9180 HPKE implementation was added on @noble primitives, replacing hpke-js as the runtime engine for both base

📁 File Classifications

packages/tsp-js/src/crypto/hpke-noble.ts

  • Type: security-critical

packages/tsp-js/src/crypto/hpke.ts

  • Type: security-critical

packages/core/src/provision/hpke.ts

  • Type: security-critical

packages/core/tests/provision.hpke-open.mjs

  • Type: test

packages/tsp-js/README.md

  • Type: documentation

packages/tsp-js/package.json

  • Type: configuration

packages/core/package.json

  • Type: configuration

package-lock.json

  • Type: configuration

packages/tsp-js/src/index.ts

  • Type: documentation

packages/tsp-js/tests/crypto.cfrg-vector.mjs

  • Type: test

packages/tsp-js/tests/crypto.hpke-js-equivalence.mjs

  • Type: test

packages/tsp-js/tests/fixtures/cfrg-auth-x25519-chacha.json

  • Type: test

🛡️ STRIDE Threat Model

Identified Threats (11)

⚪ STRIDE-1: Ephemeral Key Reuse Backdoor in hpke-noble Encap/AuthEncap

Field Detail
Category Tampering, Information Disclosure
Severity Critical
Likelihood Possible
CVSS 9.3 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-323,CWE-489,CWE-489
CAPEC CAPEC-97,CAPEC-20
OWASP A02:2021 - Cryptographic Failures

Description: function_call entry points hpke.seal/hpke.sealBase in hpke-noble.ts allow nonce/key reuse via the __unsafeFixedEphemeralSk hook due to a test-only escape hatch shipped in the production module, resulting in complete confidentiality and integrity loss for any message sealed with a fixed ephemeral key

Evidence: packages/tsp-js/src/crypto/hpke-noble.ts:95-135

type UnsafeFixedEphemeral = { __unsafeFixedEphemeralSk?: Uint8Array };
export function encap(recipientPk: Uint8Array, unsafe?: UnsafeFixedEphemeral) {
  const skE = unsafe?.__unsafeFixedEphemeralSk ?? x25519.utils.randomSecretKey();

Attack Scenario:

  1. Attacker identifies that packages/tsp-js/src/crypto/hpke-noble.ts exports encap(), authEncap(), seal(), and sealBase() all accepting an optional unsafe?: UnsafeFixedEphemeral parameter with field __unsafeFixedEphemeralSk.
  2. This module is compiled to dist/crypto/hpke-noble.js and is NOT excluded from the npm package (only package.json 'files' field lists dist/**, README.md — hpke-noble.js is bundled as an internal dependency of hpke.js).
  3. Any code with require/import access to the package internals (e.g. a compromised dependency, malicious build script, or a future maintainer copy-pasting example test code into application logic) can call seal(pt, aad, senderSk, recipientPk, info, { __unsafeFixedEphemeralSk: fixedSk }).
  4. Reusing the same skE across two seal() calls to the same recipient produces the same (key, base_nonce) pair for ChaCha20Poly1305.
  5. XORing the two resulting ciphertexts recovers the XOR of the two plaintexts, and the repeated one-time Poly1305 key can be recovered to forge future messages under that key.
  6. Confidentiality and integrity of all messages sealed under the reused ephemeral key are broken.

Preconditions: Attacker or malicious/compromised code has the ability to call the internal seal()/sealBase()/encap()/authEncap() functions with an attacker-chosen unsafe option, The unsafe parameter is not stripped from the built/published module

Existing Controls: The public hpke.ts wrapper functions (seal, open, sealBase, openBase) deliberately do not forward the unsafe parameter to callers • Extensive code comments warn 'Never pass it in production'

Recommended Mitigations: Move the __unsafeFixedEphemeralSk hook into a separate test-only module that is excluded from the published package (not compiled into dist/ shipped in the npm tarball) • Guard the unsafe path with a build-time flag stripped in production builds • Rename the parameter with an unmistakable runtime assertion (e.g., throw if NODE_ENV==='production') • Add a postinstall/CI check verifying the published tarball does not export encap/authEncap/decap/authDecap with the unsafe parameter reachable from application code


⚪ STRIDE-2: Cryptographic Implementation Substitution Risk in RFC 9180 Re-implementation

Field Detail
Category Tampering, Information Disclosure
Severity High
Likelihood Possible
CVSS 8.2 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-327,CWE-1240
CAPEC CAPEC-97
OWASP A02:2021 - Cryptographic Failures

Description: function_call entry points hpke.seal/open/sealBase/openBase in hpke-noble.ts allow silent introduction of subtle cryptographic flaws due to replacing an audited third-party HPKE library (@hpke/core + @hpke/chacha20poly1305) with a hand-rolled from-scratch RFC 9180 key schedule, resulting in potential confidentiality/integrity breaks in the core VTA sealed-transfer and TSP message encryption paths that are difficult to detect without formal cryptographic review

Evidence: packages/tsp-js/src/crypto/hpke-noble.ts:1-210

// HPKE (RFC 9180) implemented on @noble primitives — no WebCrypto.
function keySchedule(mode, sharedSecret, info) { ... }

Attack Scenario:

  1. Developer authors packages/tsp-js/src/crypto/hpke-noble.ts implementing LabeledExtract, LabeledExpand, ExtractAndExpand, KeySchedule, DH, Encap/Decap and AuthEncap/AuthDecap directly from RFC 9180 §4/§5.1 by hand.
  2. This replaces the previously used @hpke/core + @hpke/chacha20poly1305 library, which is downgraded to devDependency-only status (packages/tsp-js/package.json, packages/core/package.json).
  3. The implementation is validated only by CFRG RFC 9180 test vectors (tests/crypto.cfrg-vector.mjs) and equivalence tests against hpke-js (tests/crypto.hpke-js-equivalence.mjs) — both of which are input-shaped by the same author who wrote the implementation, and cover only the single-shot / seq=0 path.
  4. Any subtle divergence from the RFC not covered by the specific test vectors used (e.g., edge cases in low-order point rejection, KDF output-length edge cases, or AAD/nonce handling in future multi-shot use) would ship in the wallet/browser-extension production build and reach every VTA sealed-bundle decryption (COMP-004, EP-005) and TSP message operation (COMP-002, EP-001/EP-002) unnoticed.
  5. An attacker who discovers such a divergence (e.g., through differential fuzzing against the Rust affinidi-tsp/vta-sdk reference) could craft ciphertexts/bundles that decrypt incorrectly, forge messages, or in the worst case cause key/nonce reuse under attacker-influenced inputs.

Preconditions: A subtle divergence from RFC 9180 exists in the hand-rolled implementation that is not caught by the specific CFRG vectors or hpke-js equivalence tests used, Attacker has ability to submit crafted TSP messages or sealed bundles to the wallet/extension for decryption

Existing Controls: CFRG RFC 9180 mode_auth vector test with all keys fixed (tests/crypto.cfrg-vector.mjs) • Cross-implementation equivalence tests against hpke-js in both auth and base modes (tests/crypto.hpke-js-equivalence.mjs) • Mode-separation test verifying base mode ciphertexts cannot be opened as auth mode • All-zero DH shared-secret rejection (dh() function explicitly checks for all-zero output)

Recommended Mitigations: Commission an independent third-party cryptographic audit of hpke-noble.ts before merging to main/production release • Add differential fuzzing against the Rust affinidi-tsp reference implementation, not just fixed vectors • Add property-based tests (e.g., fast-check) generating random keys/AAD/plaintexts and comparing against hpke-js across many iterations, not just fixed test vectors • Pin the exact @noble/curves, @noble/hashes, @noble/ciphers versions with lockfile integrity checks and monitor upstream advisories • Retain the ability to fall back to the audited @hpke/core library behind a feature flag until the new implementation accrues production confidence


⚪ STRIDE-3: Domain-Separation Info String Drift Between JS and Rust Implementations

Field Detail
Category Tampering, Information Disclosure
Severity High
Likelihood Possible
CVSS 7.4 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-1188,CWE-345
CAPEC CAPEC-192
OWASP A08:2021 - Software and Data Integrity Failures

Description: hpkeOpen in packages/core/src/provision/hpke.ts allows cross-context ciphertext confusion due to the hardcoded HPKE_INFO string 'vta-sealed-transfer/v1' having no automated runtime cross-check against the Rust vta-sdk implementation, resulting in either broken interop or, if the check is ever loosened/removed, decryption oracle-style acceptance of ciphertexts meant for a different context

Evidence: packages/core/src/provision/hpke.ts:1-20

const HPKE_INFO = new TextEncoder().encode("vta-sealed-transfer/v1");

Attack Scenario:

  1. packages/core/src/provision/hpke.ts hardcodes const HPKE_INFO = new TextEncoder().encode('vta-sealed-transfer/v1') used as the HPKE 'info' parameter for domain separation, per RFC 9180 §5.1 KeySchedule infoHash binding.
  2. This must byte-match the Rust vta-sdk/src/sealed_transfer/hpke.rs HPKE_INFO constant, but there is no automated cross-repo test enforcing this equivalence — only a code comment.
  3. A future refactor on either the JS or Rust side (e.g., a version bump to 'vta-sealed-transfer/v2' on one side without the other) silently breaks decryption (fails closed, a DoS) OR, if a wildcard/looser info-matching scheme is later introduced by mistake, could allow a bundle sealed for a different sub-context to be accepted by hpkeOpen.
  4. An attacker who can influence which HPKE_INFO variant the recipient uses (e.g., through a supply-chain compromise of one of the two implementations, or a downgrade attack forcing use of a legacy hardcoded info string) can attempt to have a wallet open ciphertext sealed under a different security context, defeating the domain separation the info string exists to guarantee.
  5. This is confirmed by the test 'the info string is bound' in provision.hpke-open.mjs, proving the AEAD does reject mismatched info today — but only as of this snapshot, with no CI gate tying the two repos together.

Preconditions: Independent evolution of the JS and Rust codebases without a shared contract test, A future change introduces multiple valid info strings or loosens the binding

Existing Controls: HPKE info string is cryptographically bound into the AEAD key schedule (infoHash used in ksContext) so mismatches cause AEAD failure, not silent acceptance • Test 'the info string is bound' explicitly asserts version mismatch fails (provision.hpke-open.mjs)

Recommended Mitigations: Add a cross-repo CI check (e.g., a shared JSON/TOML constants file consumed by both JS and Rust builds) enforcing HPKE_INFO equality at build time • Add a runtime self-test at startup comparing a well-known test vector sealed with the pinned info string • Version the info string explicitly and reject any bundle whose info does not match a supported allow-list rather than a single hardcoded value


⚪ STRIDE-4: Missing Input Length Validation on recipientSecret in hpkeOpen

Field Detail
Category Denial of Service, Tampering
Severity Medium
Likelihood Unlikely
CVSS 5.3 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:L/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-20,CWE-1284
CAPEC CAPEC-153
OWASP A04:2021 - Insecure Design

Description: hpkeOpen function_call entry point EP-005 in packages/core/src/provision/hpke.ts allows malformed-key denial-of-service or undefined crypto behavior due to only kemEncap length being explicitly validated while recipientSecret length validation is delegated to the downstream noble library, resulting in inconsistent error surfaces and potential unhandled exceptions if the underlying library's validation ever changes silently

Evidence: packages/core/src/provision/hpke.ts:45-52

if (input.kemEncap.length !== 32) {
    throw new Error(`hpke: kemEncap must be 32 bytes (got ${input.kemEncap.length})`);
  }
  return openBase(input.ciphertext, input.aad, input.kemEncap, input.recipientSecret, HPKE_INFO);

Attack Scenario:

  1. hpkeOpen in packages/core/src/provision/hpke.ts explicitly checks input.kemEncap.length !== 32 and throws a clear error.
  2. It does not explicitly check the length/format of input.recipientSecret before calling openBase(input.ciphertext, input.aad, input.kemEncap, input.recipientSecret, HPKE_INFO).
  3. A malformed recipientSecret (wrong length, e.g., 31 bytes as tested, or a non-canonical/high-order X25519 scalar) is passed through to noble's x25519.getSharedSecret inside decap() in hpke-noble.ts.
  4. Depending on the noble library's internal validation, this either throws an unhandled low-level error (poor DX/inconsistent error taxonomy) or, in a worst case future noble version regression, silently produces a mathematically valid but application-incorrect result.
  5. Attacker-controlled bundle metadata that triggers this code path with attacker-supplied ciphertext could induce unexpected exceptions crashing the extension's decryption worker (a client-side, low-severity DoS).

Preconditions: Caller of hpkeOpen passes an attacker-influenced or malformed recipientSecret (more likely a caller bug than direct attacker control, since recipientSecret is normally the wallet's own key), noble library behavior on malformed scalars is not fully defensive

Existing Controls: kemEncap length is explicitly validated with a clear error message • Test suite (provision.hpke-open.mjs) confirms malformed kemEncap and malformed recipientSecret both currently throw (recipientSecret validated implicitly further down the stack per the test 'malformed key lengths are rejected before any crypto runs')

Recommended Mitigations: Add an explicit if (input.recipientSecret.length !== 32) throw new Error(...) check symmetric with the kemEncap check, at the top of hpkeOpen, rather than relying on downstream library behavior • Add fuzz tests feeding malformed/edge-case scalars (all-zero, all-0xff, low-order points) into recipientSecret and kemEncap


⚪ STRIDE-5: Public Subpath Export Expands Cryptographic API Attack Surface

Field Detail
Category Tampering, Elevation of Privilege
Severity Medium
Likelihood Possible
CVSS 4.8 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-1173,CWE-668
CAPEC CAPEC-227
OWASP A04:2021 - Insecure Design

Description: package_subpath_import entry point @openvtc/vti-tsp-js/hpke in packages/tsp-js/package.json allows arbitrary downstream consumers to call raw HPKE primitives outside their intended message-format context due to newly exposing crypto/hpke.ts as a standalone public export, resulting in misuse risk such as nonce/key management errors, missing envelope binding, or mode confusion when consumers bypass the higher-level TSP pack/unpack API

Evidence: packages/tsp-js/package.json:9-15

"./hpke": {
      "types": "./dist/crypto/hpke.d.ts",
      "import": "./dist/crypto/hpke.js"
    }

Attack Scenario:

  1. packages/tsp-js/package.json adds a new export map entry "./hpke": { "types": "./dist/crypto/hpke.d.ts", "import": "./dist/crypto/hpke.js" }, making low-level seal/open/sealBase/openBase directly importable by any dependent package (confirmed used by @openvtc/pnm-core's provision/hpke.ts).
  2. A third-party downstream package (or a future internal package) imports @openvtc/vti-tsp-js/hpke directly and calls sealBase/openBase without the AAD/chunk-header binding discipline used correctly in packages/core (buildChunkAad), or without ensuring each message uses a fresh random ephemeral key (which is default behavior, but a caller could misunderstand the API and attempt to cache/reuse results).
  3. Because the module now has no built-in usage restriction (any package can import * as hpke from '@openvtc/vti-tsp-js/hpke'), the security properties (single-shot only, seq=0, base_nonce unmodified, AAD-must-be-bound) depend entirely on every downstream consumer independently getting this right — a repeat of the very duplication risk the module's own comments say they are trying to avoid, now shifted to the integration layer instead of the crypto layer.
  4. A downstream consumer who omits AAD (passing an empty Uint8Array where a real chunk header should be bound) creates ciphertexts that are cryptographically valid HPKE outputs but insecure against cut-and-paste / substitution attacks within their own message format.

Preconditions: A downstream package other than the vetted packages/core consumer imports @openvtc/vti-tsp-js/hpke directly, That downstream package misuses the single-shot contract (e.g., omits proper AAD binding or misunderstands single-shot vs. multi-message sequencing)

Existing Controls: packages/core's own usage (provision/hpke.ts) correctly binds AAD via buildChunkAad and only calls openBase, not the lower-level encap/decap • Extensive JSDoc comments on sealBase/openBase describe correct usage and warn about auth vs base mode selection

Recommended Mitigations: Document explicitly in the package README that direct './hpke' subpath usage requires the caller to implement its own AAD binding, single-shot discipline, and key management — treat it as an advanced/unsafe API tier • Consider requiring an explicit opt-in flag or separate package name for the raw crypto export to reduce accidental misuse by unaware downstream consumers • Add integration tests simulating a naive downstream consumer to catch common misuse patterns (missing AAD, key reuse) with clear failure guidance


⚪ STRIDE-6: Missing Repudiation Controls for HPKE Base-Mode Anonymous Sealed Bundles

Field Detail
Category Repudiation
Severity Low
Likelihood Likely
CVSS 3.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-346,CWE-778
CAPEC CAPEC-664
OWASP A09:2021 - Security Logging and Monitoring Failures

Description: hpke.sealBase/openBase in hpke-noble.ts allows sender repudiation by design due to HPKE base mode providing no sender authentication within the AEAD/KEM itself, resulting in inability to cryptographically prove which party produced a given VTA sealed bundle if the surrounding envelope's authentication is weak, absent, or itself disputed

Evidence: packages/tsp-js/src/crypto/hpke.ts:1-20

// - Base mode (`sealBase`/`openBase`)  — VTA sealed bundles; the sender is
//     anonymous and authentication comes from the surrounding envelope.

Attack Scenario:

  1. VTA sealed bundles use HPKE base mode (mode_base 0x00) via hpke.sealBase/openBase, which by RFC 9180 design provides only recipient-side confidentiality — the KEM does not authenticate the sender (unlike mode_auth used for TSP messages).
  2. The code comments in hpke.ts state 'the sender is anonymous and authentication comes from the surrounding envelope,' meaning sender-repudiation protection is entirely delegated to a format outside the reviewed diff.
  3. If the surrounding VTA envelope's own authentication mechanism (not visible in this diff) is weak, misconfigured, or bypassed, a party could deny having sealed a given bundle, or a malicious actor with recipientPk (which is by definition public) could seal a bundle and later claim it was sealed by someone else.
  4. No logging, signature, or transcript mechanism within the reviewed hpke-noble.ts/hpke.ts/provision/hpke.ts code paths exists to independently attribute sealBase operations to a specific sender identity.

Preconditions: The surrounding VTA envelope format (outside this diff's scope) does not independently and robustly authenticate the sender, A dispute arises over the origin of a given sealed bundle

Existing Controls: Auth mode (mode_auth) is correctly used instead of base mode for TSP messages, where sender authentication matters cryptographically • Code comments explicitly document that base-mode sender authentication is a design decision delegated to the envelope layer

Recommended Mitigations: Confirm and document precisely which envelope-layer mechanism (e.g., a signature over the bundle, a TLS client cert, or an authenticated channel) provides sender accountability for VTA sealed bundles, and add integration tests proving this end-to-end • Consider migrating VTA sealed bundles to HPKE auth mode if sender non-repudiation is a desired security property, rather than leaving it fully to an unreviewed external envelope • Add structured audit logging at the point bundles are provisioned/sealed, correlating sender identity with each sealed bundle for after-the-fact accountability


⚪ STRIDE-7: Supply Chain Risk from New Direct Dependencies on @noble Cryptographic Primitives

Field Detail
Category Tampering, Information Disclosure
Severity Medium
Likelihood Possible
CVSS 5.9 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-1357,CWE-829
CAPEC CAPEC-538
OWASP A06:2021 - Vulnerable and Outdated Components

Description: npm production dependency manifest in packages/tsp-js/package.json and packages/core/package.json allows supply-chain compromise due to widening the direct-dependency trust surface to @noble/ciphers, @noble/curves, and @noble/hashes for security-critical HPKE primitives while removing the previously audited @hpke/core and @hpke/chacha20poly1305 from production dependencies, resulting in cryptographic material (keys, plaintexts) becoming reachable to any maintainer or compromised release of these lower-level packages

Evidence: packages/tsp-js/package.json:18-24

"dependencies": {
    "@noble/ciphers": "^2.0.0",
    "@noble/curves": "^2.2.0",
    "@noble/hashes": "^2.0.0"
  },

Attack Scenario:

  1. packages/tsp-js/package.json moves @hpke/chacha20poly1305 and @hpke/core to devDependencies and adds @noble/ciphers, @noble/curves, @noble/hashes as new/expanded production dependencies.
  2. packages/core/package.json similarly drops @hpke/* from production dependencies, relying transitively on @openvtc/vti-tsp-js which now pulls in @noble/ciphers directly for AEAD operations.
  3. package-lock.json shows @noble/ciphers version 2.3.0 pinned by exact integrity hash, but a future npm install without strict lockfile enforcement, or a maintainer running npm update, could pull a compromised or backdoored point release if the npm registry account for any @noble package is ever compromised (a realistic supply-chain vector demonstrated by prior real-world incidents against popular npm crypto/utility packages).
  4. Because these libraries now sit directly in the code path handling raw HPKE key material (X25519 secrets, ChaCha20Poly1305 keys, shared secrets) rather than behind a higher-level audited abstraction, a compromised @noble/ciphers or @noble/curves release could exfiltrate key material or subtly weaken cryptographic operations across every consumer of vti-tsp-js and vti-pnm-core with no additional code change required on OpenVTC's part.
  5. The blast radius is amplified because @noble packages are extremely widely used across the JS crypto ecosystem, making them an attractive high-value supply-chain target.

Preconditions: The npm registry account, CI/CD pipeline, or repository access controls for one of the @noble/* packages is compromised, Downstream build process does not strictly enforce package-lock.json integrity hashes (e.g., npm ci is not used, or lockfile verification is disabled)

Existing Controls: package-lock.json pins exact versions with SHA-512 integrity hashes for @noble/ciphers, @noble/curves, @noble/hashes • @noble packages are widely reviewed, community-audited, and maintained by a security-focused maintainer (paulmillr) with a strong track record • The equivalence tests against hpke-js provide some level of detection if @noble's underlying primitive outputs silently diverge from RFC 9180 expected values

Recommended Mitigations: Enforce npm ci (not npm install) in all build/release pipelines to guarantee lockfile integrity hash verification • Add Software Bill of Materials (SBOM) generation and automated dependency provenance/attestation verification (e.g., npm provenance, Sigstore) for @noble packages specifically given their new security-critical role • Subscribe to security advisories for @noble/ciphers, @noble/curves, @noble/hashes and configure automated alerts (e.g., GitHub Dependabot security alerts, OSV-Scanner) scoped to the crypto-critical dependency set • Consider dependency pinning with manual review gates for any @noble package version bump, given their now security-critical role, rather than automatic minor/patch updates


⚪ STRIDE-8: Missing Nonce-Reuse Protection Under Multi-Message Sequencing Extension

Field Detail
Category Tampering, Information Disclosure
Severity Medium
Likelihood Possible
CVSS 6.4 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-323,CWE-1188
CAPEC CAPEC-20
OWASP A02:2021 - Cryptographic Failures

Description: seal/open functions in hpke-noble.ts allow future catastrophic nonce reuse due to hardcoding single-shot seq=0 semantics with no sequence-number/nonce-increment API surface exposed or defended against, resulting in confidentiality and integrity collapse for ChaCha20Poly1305 if a future maintainer extends this module to multi-message use without independently re-deriving §5.2 nonce-increment logic

Evidence: packages/tsp-js/src/crypto/hpke-noble.ts:133-138

// Single-shot: seq = 0, so the nonce is base_nonce unmodified (§5.2).

Attack Scenario:

  1. The current implementation in hpke-noble.ts hardcodes single-shot behavior: 'seq = 0, so the nonce is base_nonce unmodified (§5.2)' with no Context object, no sequence counter, and no exported nonce-increment logic.
  2. A future maintainer, wanting to support multi-message TSP sessions (a very plausible extension of a message-transport protocol module currently marked '[todo]' for further layers in src/index.ts), may naively call seal() repeatedly with the same senderSk/recipientPk pair believing each call derives a fresh key schedule.
  3. Because authEncap() generates a fresh ephemeral key by default (x25519.utils.randomSecretKey()) for each call, single independent seal() calls are actually safe — but a maintainer optimizing for performance might factor out the KEM step and call keySchedule() + AEAD directly across multiple messages, exactly reproducing the RFC 9180 §5.2 seq-tracking requirement that this module does not implement or warn against at the API boundary.
  4. Without an explicit safeguard (e.g., a stateful Context class that RFC 9180 defines and that reference implementations like hpke-js provide), any future code extension is at high risk of silently reintroducing the nonce-reuse class of vulnerability this module's own comments identify as catastrophic for ChaCha20Poly1305 ('leaks the XOR of the plaintexts and the Poly1305 one-time key').

Preconditions: A future code change extends hpke-noble.ts or its callers to multi-message sequencing without re-implementing RFC 9180 §5.2 nonce increment and overflow checks, No automated test currently exists asserting that repeated calls to seal() with the same key pair always use fresh ephemeral keys (this is implicit via randomSecretKey() but not explicitly tested as a security invariant)

Existing Controls: Default (safe) behavior generates a fresh random ephemeral key per seal()/sealBase() call via x25519.utils.randomSecretKey() • Code comments extensively document the nonce-reuse risk associated with the unsafe hook • Single-shot design is documented as intentional in module header comments

Recommended Mitigations: Add an explicit unit/property test asserting that two consecutive seal() calls with identical plaintext/keys produce different ciphertexts and different enc values, codifying the fresh-ephemeral-key invariant as a regression-tested security property • If multi-message support is ever planned, implement it as a proper RFC 9180 Context object with internal seq-counter and overflow protection rather than ad-hoc reuse of the single-shot primitives • Add a code comment / lint rule flagging any future direct call to keySchedule() outside of the existing seal/open/sealBase/openBase wrappers to force explicit security review


⚪ STRIDE-9: Lack of Explicit Public Key Validation Enables Weak/Invalid Curve Point Injection

Field Detail
Category Tampering
Severity Medium
Likelihood Unlikely
CVSS 5.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-354,CWE-320
CAPEC CAPEC-459
OWASP A02:2021 - Cryptographic Failures

Description: encap/decap/authEncap/authDecap in hpke-noble.ts allow low-order/invalid X25519 point injection due to relying solely on noble's internal getSharedSecret behavior and an all-zero shared-secret check without additional explicit input point validation, resulting in potential weak shared-secret derivation if noble's default behavior for edge-case inputs ever changes or is misunderstood

Evidence: packages/tsp-js/src/crypto/hpke-noble.ts:110-114

function dh(sk: Uint8Array, pk: Uint8Array): Uint8Array {
  const shared = x25519.getSharedSecret(sk, pk);
  if (shared.every((b) => b === 0)) throw new Error("tsp: DH produced the all-zero shared secret");
  return shared;
}

Attack Scenario:

  1. decap()/authDecap() in hpke-noble.ts accept enc (attacker-controlled encapsulated key, sent over the wire in every TSP message and VTA bundle) and call dh(recipientSk, enc)x25519.getSharedSecret(sk, pk).
  2. An attacker who controls the wire-format enc value (EP-002 hpke.open, EP-004 hpke.openBase, EP-005 hpkeOpen) can submit a low-order or otherwise degenerate X25519 public key as enc.
  3. The code's only defense is the post-hoc all-zero check if (shared.every((b) => b === 0)) throw ... — this catches the classic all-zero low-order point outcome but the comment 'noble also rejects low-order points' relies entirely on an undocumented, version-dependent internal behavior of the @noble/curves library rather than an explicit, independently-verified check in this codebase.
  4. If a future @noble/curves version changes its internal low-order point handling (e.g., stops throwing/rejecting and instead returns a non-zero but still cryptographically weak shared secret for some other degenerate point), this code would silently proceed with a weak shared secret, undermining the KEM's security guarantees.
  5. This is a defense-in-depth gap: correctness currently depends on an undocumented invariant of a third-party library version rather than an explicit assertion owned by this codebase.

Preconditions: A future version of @noble/curves changes its handling of low-order/degenerate X25519 points in a way that no longer throws or zeroes out the shared secret, Attacker can supply an arbitrary enc value to a decap/open code path (true today for every message-receiving entry point)

Existing Controls: Explicit all-zero shared-secret rejection in the dh() helper function • Comment acknowledges reliance on noble's internal low-order point rejection as a secondary defense

Recommended Mitigations: Add an explicit, codebase-owned validation of the X25519 public key point (e.g., checking against the known list of low-order points per RFC 7748 §5.2, independent of noble's internal behavior) rather than relying solely on the all-zero check • Pin and monitor the exact @noble/curves version behavior for X25519 edge cases as part of the dependency update review process (see STRIDE-7 mitigations) • Add explicit unit tests feeding all known RFC 7748 low-order test points into decap/authDecap and asserting rejection, independent of the current all-zero heuristic


⚪ STRIDE-10: Test Vector Fixture Provenance Not Cryptographically Verified

Field Detail
Category Tampering, Repudiation
Severity Low
Likelihood Unlikely
CVSS 2.7 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-1357,CWE-345
CAPEC CAPEC-538
OWASP A08:2021 - Software and Data Integrity Failures

Description: crypto.cfrg-vector.mjs test entry point in packages/tsp-js/tests/fixtures/cfrg-auth-x25519-chacha.json allows a poisoned test-vector supply chain due to the fixture being manually copy-pasted from an external CFRG GitHub URL with no hash-pinning or automated re-fetch verification, resulting in a false sense of cryptographic assurance if the fixture data was ever tampered with prior to being committed

Evidence: packages/tsp-js/tests/fixtures/cfrg-auth-x25519-chacha.json:1-5

"source": "https://raw.githubusercontent.com/cfrg/draft-irtf-cfrg-hpke/master/test-vectors.json",
  "fetched": "2026-08-01",

Attack Scenario:

  1. packages/tsp-js/tests/fixtures/cfrg-auth-x25519-chacha.json embeds a comment citing its source as 'https://raw.githubusercontent.com/cfrg/draft-irtf-cfrg-hpke/master/test-vectors.json' fetched on '2026-08-01', with the vector manually filtered and copied into this repository.
  2. There is no cryptographic hash of the fetched upstream file recorded anywhere in the repo, nor an automated CI step that re-fetches and diffs against the upstream source.
  3. If the local copy was ever corrupted (accidentally or maliciously) during the copy-paste process, or if a compromised contributor intentionally altered a few bytes of the 'expected' vector values (enc, shared_secret, key, base_nonce, ct) to match a subtly broken implementation, the test suite in crypto.cfrg-vector.mjs would pass while validating an implementation that does NOT conform to the true RFC 9180 CFRG vector.
  4. This weakens the primary claimed evidence of correctness ('matching it proves the implementation is right per the standard, not merely self-consistent') because the fixture itself is trusted without independent verification at test time.
  5. Combined with STRIDE-2, a maliciously altered fixture could mask a deliberately introduced backdoor in the HPKE implementation, since the equivalence tests against hpke-js (a live library call, not a static fixture) would still need to independently agree — providing some but not complete protection.

Preconditions: A malicious insider or compromised contributor alters both the static fixture file and the implementation in a coordinated way, or the equivalence test against hpke-js is skipped/disabled in CI, No independent re-verification of the fixture against the live upstream CFRG source is performed at test/build time

Existing Controls: A second, independent test (crypto.hpke-js-equivalence.mjs) cross-checks the implementation against a live library call rather than only the static fixture, providing partial defense-in-depth • Source URL and fetch date are documented in the fixture file for auditability

Recommended Mitigations: Record the SHA-256 hash of the exact upstream test-vectors.json file that the fixture was derived from, and add a CI step that periodically re-fetches and diffs against upstream • Require two-person code review sign-off specifically on any change to committed cryptographic test fixtures • Add the fixture file to a code-owners/protected-path policy requiring additional approval for modification


⚪ STRIDE-11: No CI Enforcement Gate Confirmed for Cryptographic Equivalence Tests

Field Detail
Category Repudiation, Tampering
Severity Medium
Likelihood Possible
CVSS 6.9 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-1357,CWE-693
CAPEC CAPEC-538
OWASP A08:2021 - Software and Data Integrity Failures

Description: crypto.cfrg-vector.mjs and crypto.hpke-js-equivalence.mjs test suites allow undetected merge of a broken HPKE implementation due to no CI/CD pipeline configuration being present in the provided scope confirming these tests are enforced as required merge gates, resulting in the possibility that a future regression in hpke-noble.ts ships to production wallets and browser extensions without being caught pre-release

Evidence: N/A:N/A

N/A — no CI/CD configuration file was present in the provided source scope

Attack Scenario:

  1. This PR introduces its entire cryptographic correctness argument based on the existence of crypto.cfrg-vector.mjs and crypto.hpke-js-equivalence.mjs, both new files added by this diff.
  2. Neither file is a source file executed at runtime — they are test files that only provide assurance if actually run and enforced as a required check before merge.
  3. No GitHub Actions workflow, CI configuration, or branch protection rule was included in the reviewed scope, so it cannot be confirmed that a future PR which breaks hpke-noble.ts (e.g., an innocent refactor that reintroduces a subtle RFC 9180 deviation) would be blocked from merging.
  4. If these tests are run only manually or optionally, a regression could reach the published npm package and downstream wallets/extensions silently, with the false confidence created by the existence of 'passing' tests that were never actually re-run against the change.
  5. This compounds every other cryptographic threat in this model (STRIDE-1, STRIDE-2, STRIDE-8, STRIDE-9) since detective controls are only as strong as their enforcement.

Preconditions: The repository's actual CI/CD configuration (not visible in the provided scope) does not enforce these tests as required status checks on the default branch, A future code change to hpke-noble.ts, hpke.ts, or provision/hpke.ts is merged without the test suite being run

Existing Controls: The test files themselves are comprehensive and well-designed to catch cryptographic regressions if actually executed • Multiple independent verification methods exist (CFRG vectors + hpke-js equivalence + mode-separation tests)

Recommended Mitigations: Confirm and, if missing, add a CI workflow that runs node --test across all packages on every pull request targeting main/production branches • Configure branch protection rules requiring the crypto test suite to pass as a required status check before merge • Add a release-gating step that additionally re-runs the full crypto test suite against the final built npm package artifacts (not just source), to catch build/packaging regressions



🍝 PASTA Threat Model

Application Purpose

A browser-extension/wallet ecosystem (VTA) and its supporting TypeScript packages implement RFC 9180 HPKE encryption from scratch on @noble primitives to enable end-to-end encrypted, cross-runtime (browser, Node, React Native) message transport (TSP) and sealed identity-bundle provisioning without depending on WebCrypto, replacing a previously audited third-party HPKE library.

Inherent Risks

  • Cryptographic primitives are being hand-implemented rather than delegated to an audited library, inherently raising the bar for correctness assurance.
  • The migration touches the core confidentiality/integrity guarantee of every VTA sealed bundle and TSP message in the ecosystem.
  • Cross-language (JS/Rust) byte-compatibility contracts (HPKE_INFO, suite IDs) are enforced only by convention and tests, not by a shared source of truth.
  • A test-only unsafe API escape hatch is shipped in the same module as production code.

Objectives

Risk: Accept residual risk of a from-scratch RFC 9180 implementation only if backed by rigorous automated cross-validation (CFRG vectors + library equivalence) and, ideally, independent audit.
Business: Enable secure, wallet-agnostic verifiable credential/identity transfer between VTA and browser-extension wallets across all major JS runtimes.
Security: Guarantee confidentiality and integrity of sealed VTA bundles and TSP messages equivalent to or exceeding the previously used audited @hpke library.; Prevent any test-only or debug cryptographic hooks from being reachable in production code paths.
Financial: Avoid costly incident response and reputational damage from a cryptographic implementation flaw in a widely-distributed open-source wallet extension.
Compliance: Maintain byte-level interoperability with the Rust affinidi-tsp/vta-sdk reference implementation to avoid protocol-conformance violations.
Functional: Provide byte-compatible HPKE-Auth and HPKE-Base encryption without requiring WebCrypto or WASM, functioning identically on React Native, browser, and Node.
Operational: Maintain a single, non-duplicated RFC 9180 key-schedule implementation across the codebase to reduce maintenance burden and divergence risk.

Business Impact Analysis (3)

BIA-1: VTA Sealed Bundle Decryption (hpkeOpen) (Critical)

The wallet-side process that decrypts VTA-issued sealed identity/credential bundles using HPKE base mode so the end user can access their provisioned credentials.

MTD: 01 days 00:00 hours | RTO: 00 days 04:00 hours | RPO: 00 days 00:00 hours

  • Stakeholders: Browser Extension Users / OpenVTC Maintainers / VTA Rust SDK Team / Wallet Integrators
  • Dependencies: @openvtc/vti-tsp-js hpke module / @noble/ciphers / @noble/curves / @noble/hashes / VTA Rust sealed_transfer implementation
  • Disruptions: A cryptographic implementation defect causes all sealed bundles to fail to decrypt / A domain-separation info string mismatch between JS and Rust breaks interop / A supply-chain compromise of a @noble package corrupts or exfiltrates key material during decryption
  • Impacts: Complete inability for users to access provisioned credentials (availability impact) / Confidentiality breach of credential material if a cryptographic flaw allows unauthorized decryption / Reputational damage to OpenVTC and loss of user trust in the wallet ecosystem / Potential regulatory exposure if credentials constitute personal/identity data under GDPR or similar frameworks

BIA-2: TSP Message Seal/Open (Auth Mode) (High)

The process by which Trust Spanning Protocol messages are sealed by a sender and opened by a recipient using HPKE-Auth mode, providing sender authentication via the KEM itself.

MTD: 03 days 00:00 hours | RTO: 01 days 00:00 hours | RPO: 00 days 01:00 hours

  • Stakeholders: TSP Message Senders / TSP Message Recipients / affinidi-tsp Rust Ecosystem / OpenVTC Maintainers
  • Dependencies: @openvtc/vti-tsp-js hpke module / @noble/curves / @noble/hashes / @noble/ciphers / affinidi-tsp Rust crate
  • Disruptions: Ephemeral key reuse via the unsafe test hook is inadvertently triggered in production / A subtle RFC 9180 auth-mode deviation breaks sender authentication guarantees
  • Impacts: Loss of message confidentiality/integrity for any affected TSP session / Sender impersonation if auth-mode KEM authentication is broken / Cross-ecosystem interoperability failure with the Rust affinidi-tsp reference

BIA-3: Cryptographic Dependency Supply Chain Integrity (Medium)

The ongoing process of sourcing, updating, and verifying the @noble cryptographic packages and the @hpke reference packages used for equivalence testing.

MTD: 07 days 00:00 hours | RTO: 02 days 00:00 hours | RPO: 01 days 00:00 hours

  • Stakeholders: OpenVTC Maintainers / npm Registry / @noble Package Maintainer (paulmillr) / hpke-js Maintainers
  • Dependencies: npm registry / package-lock.json integrity hashes / CI/CD build pipeline
  • Disruptions: A compromised @noble package release is installed without lockfile enforcement / A malicious update to devDependency @hpke/core corrupts the equivalence test oracle itself
  • Impacts: Silent introduction of a cryptographic backdoor affecting all downstream consumers / Loss of confidence in the equivalence-testing safety net if the reference library itself is compromised

Technical Scope

Roles (4): RO-1 Wallet End User · RO-2 OpenVTC Package Maintainer · RO-3 Downstream Package Consumer · RO-4 npm Package Publisher (Third Party)

Actors (4): AC-1 Wallet Browser Extension Process · AC-2 OpenVTC CI Pipeline · AC-3 Downstream Package Build Process · AC-4 npm Publish Automation

Use Cases (3): VTA Sealed Bundle Provisioning and Wallet Decryption · TSP Authenticated Message Exchange · Cross-Implementation Cryptographic Conformance Verification

Attack Trees (5): SC-1: hpke-noble.ts (RFC 9180 Core Implementation) · SC-3: provision/hpke.ts (VTA Bundle Decryption) · SC-2: hpke.ts (Public HPKE Wrapper) · SC-4: @noble Cryptographic Primitive Libraries · SC-6: CFRG RFC 9180 Test Vector Fixture

Entry Points (6): EP-1 hpke.seal (Auth Mode Seal) · EP-2 hpke.open (Auth Mode Open) · EP-3 hpke.sealBase / hpke.openBase (Base Mode) · EP-4 hpkeOpen (VTA Bundle Decryption Entry) · EP-5 @openvtc/vti-tsp-js/hpke (Package Subpath Import) · EP-6 Internal Test-Only Unsafe Ephemeral Hook

Risk Registry (2): RISK-1 · RISK-2

Threat Actors (4): TA-1 Cryptographic Implementation Flaw Exploiter · TA-2 npm Supply Chain Attacker · TA-3 Malicious or Careless Downstream Integrator · TA-4 Network-Positioned Message Interceptor

Infrastructure (3): IF-1 Browser Extension Runtime (MV3 Service Worker) · IF-2 React Native Mobile Wallet Host · IF-3 npm Package Registry Infrastructure

Trust Boundaries (5): TB-1 Browser Extension / React Native Wallet Runtime · TB-2 npm Package Supply Chain · TB-3 VTA Sealed-Transfer Protocol Boundary · TB-4 TSP Message Transport Boundary · TB-5 Internal Cryptographic Module Boundary

External Entities (4): EE-1 VTA Rust sealed_transfer Sender · EE-2 TSP Peer (Sender or Recipient) · EE-3 npm Registry / @noble Package Maintainers · EE-4 hpke-js / @hpke Package Maintainers

System Components (7): SC-1 hpke-noble.ts (RFC 9180 Core Implementation) · SC-2 hpke.ts (Public HPKE Wrapper) · SC-3 provision/hpke.ts (VTA Bundle Decryption) · SC-4 @noble Cryptographic Primitive Libraries · SC-5 @hpke/core + @hpke/chacha20poly1305 (Equivalence Oracle) · SC-6 CFRG RFC 9180 Test Vector Fixture · SC-7 VTA Rust sealed_transfer Implementation

Resources And Assets (5): RA-1 Wallet X25519 Recipient Secret Key · RA-2 HPKE Shared Secret / Derived AEAD Key · RA-3 VTA Sealed Bundle Plaintext (Credentials) · RA-4 CFRG RFC 9180 Test Vector Fixture Data · RA-5 TSP Message Sender/Recipient X25519 and Ed25519 Keys

Technologies And Dependencies (5): TD-1 @noble/curves · TD-2 @noble/hashes · TD-3 @noble/ciphers · TD-4 @hpke/core + @hpke/chacha20poly1305 · TD-5 cbor-x

⚔️ Attack Scenarios (1)

Exploit identified weaknesses

flowchart LR
  S0["Ephemeral Key Reuse Backdoor in hpke-noble Encap/AuthEncap"]
  S1["Cryptographic Implementation Substitution Risk in RFC 9180 R"]
  S2["Domain-Separation Info String Drift Between JS and Rust Impl"]
  S0 --> S1
  S1 --> S2
Loading

📊 Risk Summary

Total Threats: 11

By Severity: Low: 2 · High: 2 · Medium: 6 · Critical: 1

By Category: Unknown: 11


Generated by Agentic Sec — Threat Model & Affect Analysis Agent


🔧 What to do

# Action
1 📥 Download attached reports and review the findings and threat model
2 🤖 Feed reports to your IDE copilot for fixes or security hardening suggestions
3 🛡️ Review threat model for potential risks and recommended countermeasures
4 🆘 Questions? Reach out to the Security team

🛡️ Agentic Sec — AI Security Validation Agent

@stormer78
stormer78 merged commit a5ac8c1 into OpenVTC:main Aug 17, 2026
3 checks passed
stormer78 added a commit that referenced this pull request Aug 17, 2026
Publishes the pure-TS HPKE swap (#116) and the new `./hpke` subpath export.

Minor rather than patch: `sealBase`/`openBase` and the `./hpke` export are
new public surface. The auth-mode `seal`/`open` signatures and the wire bytes
are unchanged, so existing callers need no changes.

`@openvtc/pnm-core` cannot be published until this is on npm — it imports
`@openvtc/vti-tsp-js/hpke`, and the published 0.1.0 exports map has only
".".

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78 added a commit that referenced this pull request Aug 17, 2026
* release(tsp-js): @openvtc/vti-tsp-js 0.2.0

Publishes the pure-TS HPKE swap (#116) and the new `./hpke` subpath export.

Minor rather than patch: `sealBase`/`openBase` and the `./hpke` export are
new public surface. The auth-mode `seal`/`open` signatures and the wire bytes
are unchanged, so existing callers need no changes.

`@openvtc/pnm-core` cannot be published until this is on npm — it imports
`@openvtc/vti-tsp-js/hpke`, and the published 0.1.0 exports map has only
".".

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

* release(core): @openvtc/pnm-core 0.4.0

Ships the HPKE consolidation (#116) and the trust-task-error 0.x fix (#115).

Minor rather than patch: `@hpke/*` leaves the runtime dependency set and the
`@openvtc/vti-tsp-js` range gains a floor.

The range moves from `*` to `^0.2.0`. `*` expressed no minimum, so it was
satisfied by the published 0.1.0 — which has no `./hpke` export — and a
consumer whose lockfile pinned 0.1.0 would have installed this release against
it and failed to resolve the import.

**Do not publish this until `@openvtc/vti-tsp-js@0.2.0` is on npm** (#117).

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>

---------

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
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.

3 participants