feat(tsp-js): pure-TS HPKE on @noble — one implementation for browser, Node, and React Native - #116
Conversation
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>
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.
|
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: Heads-up that I've pushed a commit to this branch ( Finishing the job in
|
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>
|
Pulled |
🛡️ AI Agentic Security Review
📊 Summary
|
| Field | Value |
|---|---|
| Repository | OpenVTC/vta-browser-plugin |
| Branch | noble-hpke → main |
| 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 stringvta-sealed-transfer/v1domain-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-hpke → main |
| 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:
- Attacker identifies that packages/tsp-js/src/crypto/hpke-noble.ts exports encap(), authEncap(), seal(), and sealBase() all accepting an optional
unsafe?: UnsafeFixedEphemeralparameter with field__unsafeFixedEphemeralSk. - 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).
- 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 }). - Reusing the same skE across two seal() calls to the same recipient produces the same (key, base_nonce) pair for ChaCha20Poly1305.
- 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.
- 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:
- 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.
- 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).
- 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.
- 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.
- 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:
- 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. - This must byte-match the Rust
vta-sdk/src/sealed_transfer/hpke.rsHPKE_INFO constant, but there is no automated cross-repo test enforcing this equivalence — only a code comment. - 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.
- 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.
- 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:
- hpkeOpen in packages/core/src/provision/hpke.ts explicitly checks
input.kemEncap.length !== 32and throws a clear error. - It does not explicitly check the length/format of
input.recipientSecretbefore callingopenBase(input.ciphertext, input.aad, input.kemEncap, input.recipientSecret, HPKE_INFO). - 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.
- 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.
- 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:
- 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). - A third-party downstream package (or a future internal package) imports
@openvtc/vti-tsp-js/hpkedirectly and callssealBase/openBasewithout 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). - 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. - 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:
- 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).
- 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.
- 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.
- 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:
- 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.
- 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.
- package-lock.json shows @noble/ciphers version 2.3.0 pinned by exact integrity hash, but a future
npm installwithout strict lockfile enforcement, or a maintainer runningnpm 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). - 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.
- 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:
- 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.
- 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. - 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. - 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:
- decap()/authDecap() in hpke-noble.ts accept
enc(attacker-controlled encapsulated key, sent over the wire in every TSP message and VTA bundle) and calldh(recipientSk, enc)→x25519.getSharedSecret(sk, pk). - An attacker who controls the wire-format
encvalue (EP-002 hpke.open, EP-004 hpke.openBase, EP-005 hpkeOpen) can submit a low-order or otherwise degenerate X25519 public key asenc. - 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. - 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.
- 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:
- 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.
- 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.
- 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.
- 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.
- 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:
- 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.
- 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.
- 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.
- 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.
- 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
📊 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
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(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>
hpke-js reaches for
crypto.subtlefor HKDF and X25519. React Native's Hermes engine has none, so@openvtc/vti-tsp-jscannot run in a React Native wallet today — and real apps polyfillsubtleonly partially (a wallet that providessubtle.digestalone 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/opensignatures and wire bytes are unchanged; the only runtime requirement iscrypto.getRandomValues(native in browsers/Node;react-native-get-random-valueson 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):tests/interop.rust-vector.mjs(the fixed-key affinidi-tsp vector)tests/crypto.cfrg-vector.mjs): AuthEncapenc+shared_secret, AuthDecap, seal reproducing the vector ciphertext, open recovering its plaintext — every key fixed, so exactly one correct outputtests/crypto.hpke-js-equivalence.mjs): each implementation opens the other's outputVerified across runtimes (probes in the Keyring reference suite):
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 touchedpackages/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 itscrypto.subtledependencies — WebAuthn, the PRF vault wrap, DID verification methods and trust-task canonicalisation still usesubtle, 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-jsnow 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.