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.
- package/README.md +118 -283
- package/dist/adaptive_switch.d.ts +41 -0
- package/dist/adaptive_switch.js +108 -0
- package/dist/http_client.js +5 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +6 -4
- package/dist/pot_frame.d.ts +59 -0
- package/dist/pot_frame.js +130 -0
- package/dist/pot_verifier.d.ts +56 -0
- package/dist/pot_verifier.js +183 -0
- package/dist/tls_binding.d.ts +52 -0
- package/dist/tls_binding.js +86 -0
- package/dist/v4_hook.d.ts +7 -2
- package/package.json +17 -11
- package/dist/dynamic_fee.d.ts +0 -75
- package/dist/dynamic_fee.js +0 -254
- package/dist/protocol_fee.d.ts +0 -72
- package/dist/protocol_fee.js +0 -199
- package/dist/revenue_tiers.d.ts +0 -36
- package/dist/revenue_tiers.js +0 -83
- package/dist/x402_enforcer.d.ts +0 -44
- package/dist/x402_enforcer.js +0 -92
|
@@ -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;
|
package/dist/http_client.js
CHANGED
|
@@ -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;
|