openttt 0.2.11 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ // sdk/src/adaptive_switch.ts — Adaptive Mode Switcher (public SDK stub)
3
+ // GRG integrity check runs server-side (helm private repo).
4
+ // This module exports types, enums, and the client-side AdaptiveSwitch
5
+ // (timestamp + ordering check only; integrity check delegated to server).
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.AdaptiveSwitch = exports.TIER_TOLERANCE_MS = exports.AdaptiveMode = void 0;
8
+ const logger_1 = require("./logger");
9
+ var AdaptiveMode;
10
+ (function (AdaptiveMode) {
11
+ AdaptiveMode["TURBO"] = "TURBO";
12
+ AdaptiveMode["FULL"] = "FULL";
13
+ })(AdaptiveMode || (exports.AdaptiveMode = AdaptiveMode = {}));
14
+ /** Tier-based dynamic tolerance (ms) */
15
+ exports.TIER_TOLERANCE_MS = {
16
+ T0_epoch: 2000,
17
+ T1_block: 200,
18
+ T2_slot: 500,
19
+ T3_micro: 10,
20
+ };
21
+ class AdaptiveSwitch {
22
+ windowSize = 20;
23
+ turboEntryThreshold = 0.95;
24
+ turboMaintainThreshold = 0.85;
25
+ history = [];
26
+ currentMode = AdaptiveMode.FULL;
27
+ minBlocks = 20;
28
+ penaltyCooldown = 0;
29
+ consecutiveFailures = 0;
30
+ tolerance;
31
+ constructor(options) {
32
+ this.tolerance = options?.tolerance ?? 100;
33
+ }
34
+ /**
35
+ * Client-side TTT check: timestamp ordering + time delta only.
36
+ * GRG integrity verification is delegated to the server (IntegrityClient).
37
+ */
38
+ verifyBlock(block, tttRecord, _chainId, _poolAddress, tier) {
39
+ const orderMatch = this.compareTransactionOrder(block.txs, tttRecord.txOrder);
40
+ const tolerance = tier ? (exports.TIER_TOLERANCE_MS[tier] ?? this.tolerance) : this.tolerance;
41
+ const timeMatch = Math.abs(block.timestamp - tttRecord.time) < tolerance;
42
+ const sequenceOk = orderMatch && timeMatch;
43
+ this.history.push(sequenceOk);
44
+ if (this.history.length > this.windowSize)
45
+ this.history.shift();
46
+ if (this.penaltyCooldown > 0)
47
+ this.penaltyCooldown--;
48
+ const matchCount = this.history.filter(h => h).length;
49
+ const matchRate = this.history.length > 0 ? matchCount / this.history.length : 0;
50
+ const effectiveThreshold = this.currentMode === AdaptiveMode.TURBO
51
+ ? this.turboMaintainThreshold
52
+ : this.turboEntryThreshold;
53
+ if (this.history.length >= this.minBlocks && matchRate >= effectiveThreshold && this.penaltyCooldown === 0) {
54
+ if (this.currentMode === AdaptiveMode.FULL) {
55
+ logger_1.logger.info(`[AdaptiveSwitch] Switching to TURBO (rate: ${(matchRate * 100).toFixed(1)}%)`);
56
+ }
57
+ this.currentMode = AdaptiveMode.TURBO;
58
+ this.consecutiveFailures = 0;
59
+ }
60
+ else {
61
+ if (this.currentMode === AdaptiveMode.TURBO) {
62
+ logger_1.logger.warn(`[AdaptiveSwitch] Switching to FULL (rate: ${(matchRate * 100).toFixed(1)}%)`);
63
+ }
64
+ this.currentMode = AdaptiveMode.FULL;
65
+ }
66
+ return this.currentMode;
67
+ }
68
+ getFeeDiscount() {
69
+ return this.currentMode === AdaptiveMode.TURBO ? 0.2 : 0.0;
70
+ }
71
+ getCurrentMode() {
72
+ return this.currentMode;
73
+ }
74
+ reset() {
75
+ this.history = [];
76
+ this.currentMode = AdaptiveMode.FULL;
77
+ this.penaltyCooldown = 0;
78
+ this.consecutiveFailures = 0;
79
+ }
80
+ serialize() {
81
+ return JSON.stringify({
82
+ history: this.history,
83
+ currentMode: this.currentMode,
84
+ consecutiveFailures: this.consecutiveFailures,
85
+ penaltyCooldown: this.penaltyCooldown,
86
+ tolerance: this.tolerance,
87
+ });
88
+ }
89
+ static deserialize(json) {
90
+ const data = JSON.parse(json);
91
+ const instance = new AdaptiveSwitch({ tolerance: data.tolerance ?? 100 });
92
+ instance.history = data.history;
93
+ instance.currentMode = data.currentMode;
94
+ instance.consecutiveFailures = data.consecutiveFailures;
95
+ instance.penaltyCooldown = data.penaltyCooldown;
96
+ return instance;
97
+ }
98
+ compareTransactionOrder(blockTxs, expectedOrder) {
99
+ if (blockTxs.length !== expectedOrder.length)
100
+ return false;
101
+ for (let i = 0; i < blockTxs.length; i++) {
102
+ if (blockTxs[i] !== expectedOrder[i])
103
+ return false;
104
+ }
105
+ return true;
106
+ }
107
+ }
108
+ exports.AdaptiveSwitch = AdaptiveSwitch;
@@ -96,6 +96,11 @@ function fetchHttpsDate(name, url, timeoutMs = 3000) {
96
96
  }
97
97
  // ---------------------------------------------------------------------------
98
98
  // HMAC helpers — default sandbox key derived from fixed chain+address strings
99
+ // HMAC key is public by design — context binding, not secrecy.
100
+ // The sandbox key below is a fixed string for local-only verification.
101
+ // In production, the HMAC key is derived from (chainId, poolAddress) and
102
+ // is deterministically recomputable by any party. The security property is
103
+ // tamper detection (integrity), not confidentiality.
99
104
  // ---------------------------------------------------------------------------
100
105
  const SANDBOX_HMAC_SECRET = "openttt-sandbox:chainId=0:address=0x0000000000000000000000000000000000000000";
101
106
  function computeHmac(timestamp, nonce, expiresAt, sources, secret = SANDBOX_HMAC_SECRET) {
package/dist/index.d.ts CHANGED
@@ -1,20 +1,19 @@
1
1
  export * from "./evm_connector";
2
- export * from "./x402_enforcer";
3
2
  export * from "./adaptive_switch";
4
- export * from "./protocol_fee";
5
3
  export * from "./pool_registry";
6
4
  export * from "./v4_hook";
7
5
  export * from "./logger";
8
6
  export * from "./types";
9
7
  export * from "./http_client";
10
8
  export * from "./time_synthesis";
11
- export * from "./dynamic_fee";
12
9
  export * from "./signer";
13
10
  export * from "./networks";
14
11
  export * from "./errors";
15
12
  export * from "./pot_signer";
16
13
  export * from "./ct_log";
17
14
  export * from "./trust_store";
18
- export * from "./revenue_tiers";
19
15
  export * from "./integrity_client";
20
16
  export * from "./osnma_source";
17
+ export * from "./tls_binding";
18
+ export * from "./pot_frame";
19
+ export * from "./pot_verifier";
package/dist/index.js CHANGED
@@ -19,11 +19,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
19
19
  // grg_forward, grg_inverse, grg_pipeline, golay, reed_solomon
20
20
  // auto_mint (GRG 의존)
21
21
  // 위 모듈은 서버 코드에서 직접 경로로 import할 것.
22
+ // 유료화 모듈 (private helm repo로 이동):
23
+ // protocol_fee, revenue_tiers, dynamic_fee, x402_enforcer
22
24
  __exportStar(require("./evm_connector"), exports);
23
- __exportStar(require("./x402_enforcer"), exports);
24
25
  __exportStar(require("./adaptive_switch"), exports);
25
26
  // ttt_builder omitted — server-internal only
26
- __exportStar(require("./protocol_fee"), exports);
27
27
  __exportStar(require("./pool_registry"), exports);
28
28
  __exportStar(require("./v4_hook"), exports);
29
29
  __exportStar(require("./logger"), exports);
@@ -31,13 +31,15 @@ __exportStar(require("./types"), exports);
31
31
  // ttt_client omitted — requires auto_mint (server-internal, not in public SDK)
32
32
  __exportStar(require("./http_client"), exports);
33
33
  __exportStar(require("./time_synthesis"), exports);
34
- __exportStar(require("./dynamic_fee"), exports);
35
34
  __exportStar(require("./signer"), exports);
36
35
  __exportStar(require("./networks"), exports);
37
36
  __exportStar(require("./errors"), exports);
38
37
  __exportStar(require("./pot_signer"), exports);
39
38
  __exportStar(require("./ct_log"), exports);
40
39
  __exportStar(require("./trust_store"), exports);
41
- __exportStar(require("./revenue_tiers"), exports);
42
40
  __exportStar(require("./integrity_client"), exports);
43
41
  __exportStar(require("./osnma_source"), exports);
42
+ // TTTPS -02: TLS binding_key implementation (draft-helmprotocol-tttps-02 Section 7.1)
43
+ __exportStar(require("./tls_binding"), exports);
44
+ __exportStar(require("./pot_frame"), exports);
45
+ __exportStar(require("./pot_verifier"), exports);
@@ -0,0 +1,59 @@
1
+ /**
2
+ * pot_frame.ts — TTTPS PoT Frame encoder/decoder
3
+ *
4
+ * Wire format per draft-helmprotocol-tttps-02 Section 7.1:
5
+ *
6
+ * +----------------------------------+
7
+ * | binding_key (32 bytes) | ← TLS Exporter output
8
+ * +----------------------------------+
9
+ * | pot_record (143 bytes) | ← Section 4.1
10
+ * | Version[1] Tier[1] Res[1] |
11
+ * | Timestamp[8] |
12
+ * | Confidence[4] |
13
+ * | Nonce[32] |
14
+ * | GRGCommit[32] |
15
+ * | Ed25519Sig[64] |
16
+ * +----------------------------------+
17
+ * Total: 175 bytes
18
+ *
19
+ * pot_record_without_sig = first 79 bytes of pot_record
20
+ * (used as TLS Exporter context)
21
+ */
22
+ export declare const POT_RECORD_SIZE = 143;
23
+ export declare const POT_WITHOUT_SIG_SIZE = 79;
24
+ export declare const BINDING_KEY_SIZE = 32;
25
+ export declare const POT_FRAME_SIZE = 175;
26
+ export interface PotFrame {
27
+ bindingKey: Buffer;
28
+ potRecord: Buffer;
29
+ version: number;
30
+ tier: number;
31
+ reserved: number;
32
+ timestamp: bigint;
33
+ confidence: number;
34
+ nonce: Buffer;
35
+ grgCommit: Buffer;
36
+ ed25519Sig: Buffer;
37
+ }
38
+ export declare class PotFrameError extends Error {
39
+ constructor(message: string);
40
+ }
41
+ /**
42
+ * Encode a PoT frame (175 bytes) from components.
43
+ */
44
+ export declare function encodePotFrame(bindingKey: Buffer, potRecord: Buffer): Buffer;
45
+ /**
46
+ * Decode a PoT frame (175 bytes) into components.
47
+ */
48
+ export declare function decodePotFrame(frame: Buffer): PotFrame;
49
+ /**
50
+ * Extract the portion of pot_record EXCLUDING the signature.
51
+ * This is used as the TLS Exporter context (Section 7.1).
52
+ * = first 79 bytes of the 143-byte pot_record
53
+ */
54
+ export declare function potRecordWithoutSig(potRecord: Buffer): Buffer;
55
+ /**
56
+ * Validate basic frame structure without TLS verification.
57
+ * (Full verification requires a TLS socket — see tls_binding.ts)
58
+ */
59
+ export declare function validatePotFrameStructure(frame: Buffer): void;
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ /**
3
+ * pot_frame.ts — TTTPS PoT Frame encoder/decoder
4
+ *
5
+ * Wire format per draft-helmprotocol-tttps-02 Section 7.1:
6
+ *
7
+ * +----------------------------------+
8
+ * | binding_key (32 bytes) | ← TLS Exporter output
9
+ * +----------------------------------+
10
+ * | pot_record (143 bytes) | ← Section 4.1
11
+ * | Version[1] Tier[1] Res[1] |
12
+ * | Timestamp[8] |
13
+ * | Confidence[4] |
14
+ * | Nonce[32] |
15
+ * | GRGCommit[32] |
16
+ * | Ed25519Sig[64] |
17
+ * +----------------------------------+
18
+ * Total: 175 bytes
19
+ *
20
+ * pot_record_without_sig = first 79 bytes of pot_record
21
+ * (used as TLS Exporter context)
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.PotFrameError = exports.POT_FRAME_SIZE = exports.BINDING_KEY_SIZE = exports.POT_WITHOUT_SIG_SIZE = exports.POT_RECORD_SIZE = void 0;
25
+ exports.encodePotFrame = encodePotFrame;
26
+ exports.decodePotFrame = decodePotFrame;
27
+ exports.potRecordWithoutSig = potRecordWithoutSig;
28
+ exports.validatePotFrameStructure = validatePotFrameStructure;
29
+ exports.POT_RECORD_SIZE = 143; // bytes
30
+ exports.POT_WITHOUT_SIG_SIZE = 79; // bytes (143 - 64 sig)
31
+ exports.BINDING_KEY_SIZE = 32; // bytes
32
+ exports.POT_FRAME_SIZE = 175; // bytes (32 + 143)
33
+ class PotFrameError extends Error {
34
+ constructor(message) {
35
+ super(message);
36
+ this.name = "PotFrameError";
37
+ }
38
+ }
39
+ exports.PotFrameError = PotFrameError;
40
+ /**
41
+ * Encode a PoT frame (175 bytes) from components.
42
+ */
43
+ function encodePotFrame(bindingKey, potRecord) {
44
+ if (bindingKey.length !== exports.BINDING_KEY_SIZE) {
45
+ throw new PotFrameError(`binding_key must be ${exports.BINDING_KEY_SIZE} bytes, got ${bindingKey.length}`);
46
+ }
47
+ if (potRecord.length !== exports.POT_RECORD_SIZE) {
48
+ throw new PotFrameError(`pot_record must be ${exports.POT_RECORD_SIZE} bytes, got ${potRecord.length}`);
49
+ }
50
+ const frame = Buffer.allocUnsafe(exports.POT_FRAME_SIZE);
51
+ bindingKey.copy(frame, 0);
52
+ potRecord.copy(frame, exports.BINDING_KEY_SIZE);
53
+ return frame;
54
+ }
55
+ /**
56
+ * Decode a PoT frame (175 bytes) into components.
57
+ */
58
+ function decodePotFrame(frame) {
59
+ if (frame.length !== exports.POT_FRAME_SIZE) {
60
+ throw new PotFrameError(`PoT frame must be ${exports.POT_FRAME_SIZE} bytes, got ${frame.length}`);
61
+ }
62
+ const bindingKey = frame.subarray(0, exports.BINDING_KEY_SIZE);
63
+ const potRecord = frame.subarray(exports.BINDING_KEY_SIZE, exports.POT_FRAME_SIZE);
64
+ // Parse pot_record fields (big-endian per Section 4.1)
65
+ let offset = 0;
66
+ // Byte 0: Version[4 bits] | Tier[4 bits]
67
+ const versionTier = potRecord.readUInt8(offset++);
68
+ const version = (versionTier >> 4) & 0x0F;
69
+ const tier = versionTier & 0x0F;
70
+ // Byte 1: Source Count (ignored in frame, stored separately)
71
+ const _sourceCount = potRecord.readUInt8(offset++);
72
+ // Byte 2: Reserved
73
+ const reserved = potRecord.readUInt8(offset++);
74
+ // Bytes 3-10: Timestamp (64-bit BE, nanoseconds)
75
+ const timestamp = potRecord.readBigUInt64BE(offset);
76
+ offset += 8;
77
+ // Bytes 11-14: Confidence (32-bit BE, ppm)
78
+ const confidence = potRecord.readUInt32BE(offset);
79
+ offset += 4;
80
+ // Bytes 15-46: Nonce (32 bytes)
81
+ const nonce = Buffer.from(potRecord.subarray(offset, offset + 32));
82
+ offset += 32;
83
+ // Bytes 47-78: GRG Commitment (32 bytes)
84
+ const grgCommit = Buffer.from(potRecord.subarray(offset, offset + 32));
85
+ offset += 32;
86
+ // Bytes 79-142: Ed25519 Signature (64 bytes)
87
+ const ed25519Sig = Buffer.from(potRecord.subarray(offset, offset + 64));
88
+ return {
89
+ bindingKey: Buffer.from(bindingKey),
90
+ potRecord: Buffer.from(potRecord),
91
+ version,
92
+ tier,
93
+ reserved,
94
+ timestamp,
95
+ confidence,
96
+ nonce,
97
+ grgCommit,
98
+ ed25519Sig,
99
+ };
100
+ }
101
+ /**
102
+ * Extract the portion of pot_record EXCLUDING the signature.
103
+ * This is used as the TLS Exporter context (Section 7.1).
104
+ * = first 79 bytes of the 143-byte pot_record
105
+ */
106
+ function potRecordWithoutSig(potRecord) {
107
+ if (potRecord.length !== exports.POT_RECORD_SIZE) {
108
+ throw new PotFrameError(`pot_record must be ${exports.POT_RECORD_SIZE} bytes`);
109
+ }
110
+ return Buffer.from(potRecord.subarray(0, exports.POT_WITHOUT_SIG_SIZE));
111
+ }
112
+ /**
113
+ * Validate basic frame structure without TLS verification.
114
+ * (Full verification requires a TLS socket — see tls_binding.ts)
115
+ */
116
+ function validatePotFrameStructure(frame) {
117
+ if (frame.length !== exports.POT_FRAME_SIZE) {
118
+ throw new PotFrameError(`Expected ${exports.POT_FRAME_SIZE} bytes, got ${frame.length}`);
119
+ }
120
+ const parsed = decodePotFrame(frame);
121
+ if (parsed.version !== 1) {
122
+ throw new PotFrameError(`Unknown PoT version: ${parsed.version}. Expected 1.`);
123
+ }
124
+ if (parsed.tier > 3) {
125
+ throw new PotFrameError(`Invalid tier: ${parsed.tier}. Valid range 0-3.`);
126
+ }
127
+ if (parsed.reserved !== 0) {
128
+ throw new PotFrameError(`Reserved field must be 0x00, got 0x${parsed.reserved.toString(16)}`);
129
+ }
130
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * pot_verifier.ts — Complete TTTPS PoT frame verification
3
+ *
4
+ * Implements the normative verification sequence from
5
+ * draft-helmprotocol-tttps-02 Section 4.5 + Section 7.1:
6
+ *
7
+ * 1. Version check
8
+ * 2. TLS binding_key check ← NEW in -02 (Ekr fix)
9
+ * 3. HMAC context gate (~6 μs)
10
+ * 4. Ed25519 signature (~100 μs)
11
+ * 5. Recency check
12
+ * 6. Nonce freshness
13
+ *
14
+ * Step 2 is checked FIRST because it's O(1) constant-time
15
+ * and prevents cross-session replay before any crypto.
16
+ */
17
+ import * as tls from "tls";
18
+ import { PotFrame } from "./pot_frame";
19
+ export type VerifyError = "UNKNOWN_VERSION" | "BINDING_KEY_MISMATCH" | "HMAC_CONTEXT_FAILURE" | "SIGNATURE_INVALID" | "SUBMISSION_OUTSIDE_TOLERANCE" | "NONCE_REPLAY" | "FRAME_MALFORMED";
20
+ export interface VerifyResult {
21
+ valid: boolean;
22
+ error?: VerifyError;
23
+ frame?: PotFrame;
24
+ latencyMs?: number;
25
+ }
26
+ export interface VerifierConfig {
27
+ /** Ed25519 public key of the trusted PoT Issuer (hex or Buffer) */
28
+ issuerPublicKey: Buffer | string;
29
+ /** Non-recoverable nonce cache (implement as LRU or Set with TTL) */
30
+ nonceCache: {
31
+ has(nonce: string): boolean;
32
+ add(nonce: string): void;
33
+ };
34
+ }
35
+ export declare class PotVerifier {
36
+ private issuerPubKey;
37
+ private nonceCache;
38
+ constructor(config: VerifierConfig);
39
+ /**
40
+ * Verify a 175-byte PoT frame over an active TLS socket.
41
+ *
42
+ * @param socket - The TLS socket on which the frame was received
43
+ * @param frame - 175-byte PoT frame (binding_key[32] + pot_record[143])
44
+ * @param nowMs - Current time in milliseconds (injectable for testing)
45
+ */
46
+ verify(socket: tls.TLSSocket, frame: Buffer, nowMs?: number): VerifyResult;
47
+ private verifyEd25519;
48
+ }
49
+ export declare class MemoryNonceCache {
50
+ private cache;
51
+ private ttlMs;
52
+ constructor(ttlMs?: number);
53
+ has(nonce: string): boolean;
54
+ add(nonce: string): void;
55
+ private sweep;
56
+ }
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ /**
3
+ * pot_verifier.ts — Complete TTTPS PoT frame verification
4
+ *
5
+ * Implements the normative verification sequence from
6
+ * draft-helmprotocol-tttps-02 Section 4.5 + Section 7.1:
7
+ *
8
+ * 1. Version check
9
+ * 2. TLS binding_key check ← NEW in -02 (Ekr fix)
10
+ * 3. HMAC context gate (~6 μs)
11
+ * 4. Ed25519 signature (~100 μs)
12
+ * 5. Recency check
13
+ * 6. Nonce freshness
14
+ *
15
+ * Step 2 is checked FIRST because it's O(1) constant-time
16
+ * and prevents cross-session replay before any crypto.
17
+ */
18
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ var desc = Object.getOwnPropertyDescriptor(m, k);
21
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
22
+ desc = { enumerable: true, get: function() { return m[k]; } };
23
+ }
24
+ Object.defineProperty(o, k2, desc);
25
+ }) : (function(o, m, k, k2) {
26
+ if (k2 === undefined) k2 = k;
27
+ o[k2] = m[k];
28
+ }));
29
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
30
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
31
+ }) : function(o, v) {
32
+ o["default"] = v;
33
+ });
34
+ var __importStar = (this && this.__importStar) || (function () {
35
+ var ownKeys = function(o) {
36
+ ownKeys = Object.getOwnPropertyNames || function (o) {
37
+ var ar = [];
38
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
39
+ return ar;
40
+ };
41
+ return ownKeys(o);
42
+ };
43
+ return function (mod) {
44
+ if (mod && mod.__esModule) return mod;
45
+ var result = {};
46
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
47
+ __setModuleDefault(result, mod);
48
+ return result;
49
+ };
50
+ })();
51
+ Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.MemoryNonceCache = exports.PotVerifier = void 0;
53
+ const crypto = __importStar(require("crypto"));
54
+ const pot_frame_1 = require("./pot_frame");
55
+ const tls_binding_1 = require("./tls_binding");
56
+ /** Tier tolerance windows in milliseconds (Section 8) — numeric tier ID */
57
+ const TTTPS_TIER_TOLERANCE_MS = {
58
+ 0: 60_000, // T0_epoch: 60s
59
+ 1: 2_000, // T1_block: 2s
60
+ 2: 12_000, // T2_slot: 12s
61
+ 3: 100, // T3_micro: 100ms
62
+ };
63
+ class PotVerifier {
64
+ issuerPubKey;
65
+ nonceCache;
66
+ constructor(config) {
67
+ this.issuerPubKey = typeof config.issuerPublicKey === "string"
68
+ ? Buffer.from(config.issuerPublicKey, "hex")
69
+ : config.issuerPublicKey;
70
+ this.nonceCache = config.nonceCache;
71
+ }
72
+ /**
73
+ * Verify a 175-byte PoT frame over an active TLS socket.
74
+ *
75
+ * @param socket - The TLS socket on which the frame was received
76
+ * @param frame - 175-byte PoT frame (binding_key[32] + pot_record[143])
77
+ * @param nowMs - Current time in milliseconds (injectable for testing)
78
+ */
79
+ verify(socket, frame, nowMs = Date.now()) {
80
+ // ── Structural check ──────────────────────────────────────────
81
+ if (frame.length !== pot_frame_1.POT_FRAME_SIZE) {
82
+ return { valid: false, error: "FRAME_MALFORMED" };
83
+ }
84
+ let parsed;
85
+ try {
86
+ parsed = (0, pot_frame_1.decodePotFrame)(frame);
87
+ }
88
+ catch {
89
+ return { valid: false, error: "FRAME_MALFORMED" };
90
+ }
91
+ // ── Step 1: Version check ──────────────────────────────────────
92
+ if (parsed.version !== 1) {
93
+ return { valid: false, error: "UNKNOWN_VERSION" };
94
+ }
95
+ // ── Step 2: TLS binding_key (NEW — Ekr Section 7.1 fix) ───────
96
+ // O(1), constant-time. Must precede all crypto to prevent
97
+ // cross-session replay amplification.
98
+ const potWithoutSig = (0, pot_frame_1.potRecordWithoutSig)(parsed.potRecord);
99
+ const bindingValid = (0, tls_binding_1.verifyBindingKey)(socket, potWithoutSig, parsed.bindingKey);
100
+ if (!bindingValid) {
101
+ return { valid: false, error: "BINDING_KEY_MISMATCH", frame: parsed };
102
+ }
103
+ // ── Step 3: HMAC context gate (~6 μs) ─────────────────────────
104
+ // NOTE: Actual GRG HMAC verification is done server-side via
105
+ // IntegrityClient (integrity.helmprotocol.com).
106
+ // Local verification checks Ed25519 over grgCommit.
107
+ // Full HMAC verification is available when running Helm server.
108
+ // ── Step 4: Ed25519 signature verification (~100 μs) ──────────
109
+ const signatureValid = this.verifyEd25519(parsed.potRecord.subarray(0, 79), // pot_without_sig
110
+ parsed.ed25519Sig);
111
+ if (!signatureValid) {
112
+ return { valid: false, error: "SIGNATURE_INVALID", frame: parsed };
113
+ }
114
+ // ── Step 5: Recency check (AdaptiveSwitch gate) ────────────────
115
+ const potTimestampMs = Number(parsed.timestamp / 1000000n);
116
+ const latencyMs = nowMs - potTimestampMs;
117
+ const tolerance = TTTPS_TIER_TOLERANCE_MS[parsed.tier] ?? TTTPS_TIER_TOLERANCE_MS[1];
118
+ if (latencyMs > tolerance) {
119
+ // Delay attack detected → triggers FULL mode in AdaptiveSwitch
120
+ return {
121
+ valid: false,
122
+ error: "SUBMISSION_OUTSIDE_TOLERANCE",
123
+ frame: parsed,
124
+ latencyMs,
125
+ };
126
+ }
127
+ // ── Step 6: Nonce freshness ────────────────────────────────────
128
+ const nonceHex = parsed.nonce.toString("hex");
129
+ if (this.nonceCache.has(nonceHex)) {
130
+ return { valid: false, error: "NONCE_REPLAY", frame: parsed };
131
+ }
132
+ this.nonceCache.add(nonceHex);
133
+ return { valid: true, frame: parsed, latencyMs };
134
+ }
135
+ verifyEd25519(data, signature) {
136
+ try {
137
+ return crypto.verify(null, data, {
138
+ key: this.issuerPubKey,
139
+ format: "der",
140
+ type: "spki",
141
+ }, signature);
142
+ }
143
+ catch {
144
+ return false;
145
+ }
146
+ }
147
+ }
148
+ exports.PotVerifier = PotVerifier;
149
+ // ──────────────────────────────────────────────────────────────────
150
+ // Simple in-memory nonce cache with TTL (for testing / small deployments)
151
+ // Production: use Redis with SETEX
152
+ // ──────────────────────────────────────────────────────────────────
153
+ class MemoryNonceCache {
154
+ cache = new Map(); // nonce → expiresAt
155
+ ttlMs;
156
+ constructor(ttlMs = 120_000) {
157
+ this.ttlMs = ttlMs;
158
+ }
159
+ has(nonce) {
160
+ const exp = this.cache.get(nonce);
161
+ if (exp === undefined)
162
+ return false;
163
+ if (Date.now() > exp) {
164
+ this.cache.delete(nonce);
165
+ return false;
166
+ }
167
+ return true;
168
+ }
169
+ add(nonce) {
170
+ // Periodic cleanup (keep cache from growing unbounded)
171
+ if (this.cache.size > 10_000)
172
+ this.sweep();
173
+ this.cache.set(nonce, Date.now() + this.ttlMs);
174
+ }
175
+ sweep() {
176
+ const now = Date.now();
177
+ for (const [k, exp] of this.cache) {
178
+ if (now > exp)
179
+ this.cache.delete(k);
180
+ }
181
+ }
182
+ }
183
+ exports.MemoryNonceCache = MemoryNonceCache;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * tls_binding.ts — RFC 5705 TLS Exporter binding for TTTPS
3
+ *
4
+ * Implements Section 7.1 of draft-helmprotocol-tttps-02:
5
+ * binding_key = TLS-Exporter("EXPORTER-tttps-pot-binding",
6
+ * pot_record_without_sig, 32)
7
+ *
8
+ * Both client and server derive the same 32-byte key from the
9
+ * shared TLS session master secret. A PoT frame generated in
10
+ * session A cannot be replayed into session B.
11
+ *
12
+ * Node.js API: tls.TLSSocket.exportKeyingMaterial()
13
+ * Available since Node.js 13.x (LTS 14+)
14
+ * RFC 5705 compliant.
15
+ */
16
+ import * as tls from "tls";
17
+ /** Label defined in draft-helmprotocol-tttps-02 Section 7.1 */
18
+ export declare const TTTPS_EXPORTER_LABEL = "EXPORTER-tttps-pot-binding";
19
+ export declare const TTTPS_BINDING_KEY_LENGTH = 32;
20
+ export interface BindingKeyResult {
21
+ bindingKey: Buffer;
22
+ sessionId: string;
23
+ }
24
+ export declare class TLSBindingError extends Error {
25
+ constructor(message: string);
26
+ }
27
+ /**
28
+ * Derive the 32-byte binding key from a TLS session and a PoT record.
29
+ *
30
+ * @param socket - Active TLS socket (must be in CONNECTED state)
31
+ * @param potWithoutSig - PoT record bytes WITHOUT the Ed25519 signature
32
+ * (first 79 bytes of the 143-byte record:
33
+ * Version[1] + Tier[1] + Reserved[1] +
34
+ * Timestamp[8] + Confidence[4] +
35
+ * Nonce[32] + GRGCommit[32])
36
+ * @returns 32-byte binding key
37
+ */
38
+ export declare function computeBindingKey(socket: tls.TLSSocket, potWithoutSig: Buffer): BindingKeyResult;
39
+ /**
40
+ * Verify that a received binding_key matches the expected value
41
+ * for this TLS session and PoT record.
42
+ *
43
+ * This is the normative check from draft-helmprotocol-tttps-02 Section 7.1:
44
+ * "The verifier MUST recompute expected_key via TLS-Exporter
45
+ * and verify it matches the binding_key in the PoT frame header."
46
+ *
47
+ * @param socket - Active TLS socket (same session as sender)
48
+ * @param potWithoutSig - PoT record bytes WITHOUT signature (79 bytes)
49
+ * @param receivedBindingKey - 32-byte binding_key from the received PoT frame
50
+ * @returns true if binding is valid, false if cross-session replay detected
51
+ */
52
+ export declare function verifyBindingKey(socket: tls.TLSSocket, potWithoutSig: Buffer, receivedBindingKey: Buffer): boolean;