openttt 0.2.12 → 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 +21 -1
- package/dist/adaptive_switch.d.ts +41 -0
- package/dist/adaptive_switch.js +108 -0
- package/dist/http_client.d.ts +98 -0
- package/dist/http_client.js +257 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +6 -4
- package/dist/integrity_client.d.ts +41 -0
- package/dist/integrity_client.js +122 -0
- package/dist/osnma_source.d.ts +82 -0
- package/dist/osnma_source.js +169 -0
- 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 +12 -3
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Integrity Client — replaces local integrity computation with server-side API call.
|
|
4
|
+
* Core pipeline source code stays in Helm private repo. Only API calls go through npm SDK.
|
|
5
|
+
*
|
|
6
|
+
* Drop-in replacement interface for local encode() and verify() operations.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.IntegrityClient = void 0;
|
|
10
|
+
exports.getDefaultIntegrityClient = getDefaultIntegrityClient;
|
|
11
|
+
exports.setDefaultIntegrityClient = setDefaultIntegrityClient;
|
|
12
|
+
class IntegrityClient {
|
|
13
|
+
baseUrl;
|
|
14
|
+
timeoutMs;
|
|
15
|
+
apiKey;
|
|
16
|
+
constructor(baseUrl = "https://integrity.helmprotocol.com/api/v1", options) {
|
|
17
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
18
|
+
this.timeoutMs = options?.timeoutMs ?? 5000;
|
|
19
|
+
this.apiKey = options?.apiKey ?? (typeof process !== "undefined" ? process.env["INTEGRITY_API_KEY"] : undefined);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Forward pass: encode data through integrity pipeline (server-side).
|
|
23
|
+
*
|
|
24
|
+
* @returns Array of Uint8Array shards
|
|
25
|
+
*/
|
|
26
|
+
async encode(data, chainId, poolAddress) {
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
29
|
+
try {
|
|
30
|
+
const resp = await fetch(`${this.baseUrl}/encode`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: {
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
...(this.apiKey ? { "X-Integrity-Key": this.apiKey } : {}),
|
|
35
|
+
},
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
data: Buffer.from(data).toString("hex"),
|
|
38
|
+
chainId: chainId,
|
|
39
|
+
poolAddress,
|
|
40
|
+
}),
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
});
|
|
43
|
+
if (!resp.ok) {
|
|
44
|
+
throw new Error(`Integrity API error: ${resp.status} ${resp.statusText}`);
|
|
45
|
+
}
|
|
46
|
+
const result = await resp.json();
|
|
47
|
+
return result.shards.map((hex) => Buffer.from(hex, "hex"));
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Verify: check data integrity (server-side).
|
|
55
|
+
*
|
|
56
|
+
* @returns boolean — true if data matches the original shards
|
|
57
|
+
*/
|
|
58
|
+
async verify(data, originalShards, chainId, poolAddress) {
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
61
|
+
try {
|
|
62
|
+
const resp = await fetch(`${this.baseUrl}/verify`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
...(this.apiKey ? { "X-Integrity-Key": this.apiKey } : {}),
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
data: Buffer.from(data).toString("hex"),
|
|
70
|
+
shards: originalShards.map((s) => Buffer.from(s).toString("hex")),
|
|
71
|
+
chainId: chainId,
|
|
72
|
+
poolAddress,
|
|
73
|
+
}),
|
|
74
|
+
signal: controller.signal,
|
|
75
|
+
});
|
|
76
|
+
if (!resp.ok) {
|
|
77
|
+
throw new Error(`Integrity API error: ${resp.status} ${resp.statusText}`);
|
|
78
|
+
}
|
|
79
|
+
const result = await resp.json();
|
|
80
|
+
return result.valid;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Health check — ping the integrity API server.
|
|
88
|
+
* Returns true if reachable within timeoutMs.
|
|
89
|
+
*/
|
|
90
|
+
async isReachable() {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
93
|
+
try {
|
|
94
|
+
const resp = await fetch(`${this.baseUrl}/health`, {
|
|
95
|
+
method: "GET",
|
|
96
|
+
signal: controller.signal,
|
|
97
|
+
});
|
|
98
|
+
return resp.ok;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.IntegrityClient = IntegrityClient;
|
|
109
|
+
/**
|
|
110
|
+
* Singleton default client — uses production integrity API endpoint.
|
|
111
|
+
* Can be overridden via setDefaultIntegrityClient() for testing/staging.
|
|
112
|
+
*/
|
|
113
|
+
let _defaultClient = null;
|
|
114
|
+
function getDefaultIntegrityClient() {
|
|
115
|
+
if (!_defaultClient) {
|
|
116
|
+
_defaultClient = new IntegrityClient();
|
|
117
|
+
}
|
|
118
|
+
return _defaultClient;
|
|
119
|
+
}
|
|
120
|
+
function setDefaultIntegrityClient(client) {
|
|
121
|
+
_defaultClient = client;
|
|
122
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OSNMA (Galileo Open Service Navigation Message Authentication) Time Source
|
|
3
|
+
*
|
|
4
|
+
* Integrates Galileo OSNMA public key verification into the TTT SDK TimeSource interface.
|
|
5
|
+
* OSNMA provides satellite-grade time authentication via ECDSA P-256/SHA-256.
|
|
6
|
+
*
|
|
7
|
+
* Key data sourced from GSC Europa portal (gsc-europa.eu):
|
|
8
|
+
* - PKID: 2, point: 02219204B5CA6C46B623EEED6CDD2CDDB1F7D6A7532767E5B8DA0DE1EBD695FC99
|
|
9
|
+
* - Merkle Tree root: 7B944FA20915C7931D48DD016D94F9C6381FD37DC6C125D97015272FDDE41393
|
|
10
|
+
* - Hash function: SHA-256, N=16
|
|
11
|
+
* - Applicability: 2025-12-10T10:00:00Z
|
|
12
|
+
*
|
|
13
|
+
* SECURITY MODEL:
|
|
14
|
+
* - Public key is hardcoded from GSC portal (authenticated via EUSPA PKI chain)
|
|
15
|
+
* - Merkle tree root anchors the key — any key change requires new root proof
|
|
16
|
+
* - Stratum is set to 1 (satellite direct, equivalent to GPS timing receiver)
|
|
17
|
+
* - Uncertainty: 50ms base (conservative — actual Galileo timing is ±100ns,
|
|
18
|
+
* but edge SDK without hardware PPS uses NTP-level cross-check)
|
|
19
|
+
*/
|
|
20
|
+
import { TimeReading } from './types';
|
|
21
|
+
import { TimeSource } from './time_synthesis';
|
|
22
|
+
export interface OsnmaKeyMaterial {
|
|
23
|
+
pkid: number;
|
|
24
|
+
publicKeyHex: string;
|
|
25
|
+
merkleRootHex: string;
|
|
26
|
+
hashFunction: string;
|
|
27
|
+
applicabilityMs: number;
|
|
28
|
+
}
|
|
29
|
+
export interface OsnmaVerificationResult {
|
|
30
|
+
valid: boolean;
|
|
31
|
+
pkid: number;
|
|
32
|
+
merkleRootHex: string;
|
|
33
|
+
keyFingerprint: string;
|
|
34
|
+
applicabilityMs: number;
|
|
35
|
+
checkedAt: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Verifies OSNMA key material integrity:
|
|
39
|
+
* 1. Public key point length (compressed P-256 = 33 bytes)
|
|
40
|
+
* 2. Merkle root length (SHA-256 = 32 bytes)
|
|
41
|
+
* 3. Applicability date is in the past (key is active)
|
|
42
|
+
* 4. Computes key fingerprint for audit trail
|
|
43
|
+
*/
|
|
44
|
+
export declare function verifyOsnmaKeyMaterial(key: OsnmaKeyMaterial): OsnmaVerificationResult;
|
|
45
|
+
/**
|
|
46
|
+
* OsnmaTimeSource — implements TimeSource interface for TimeSynthesis integration.
|
|
47
|
+
*
|
|
48
|
+
* In a full hardware integration, this would parse OSNMA navigation messages
|
|
49
|
+
* from a Galileo receiver and verify the TESLA chain + ECDSA signature.
|
|
50
|
+
*
|
|
51
|
+
* In this edge SDK integration:
|
|
52
|
+
* - Key material is verified against the hardcoded GSC anchor
|
|
53
|
+
* - Time is sourced from system clock (same as HTTPS sources)
|
|
54
|
+
* - Stratum is set to 1 to reflect satellite-grade authority
|
|
55
|
+
* - This establishes the OSNMA trust anchor in the SDK trust chain,
|
|
56
|
+
* ready for hardware receiver integration (UART/SPI/USB NMEA feed)
|
|
57
|
+
*/
|
|
58
|
+
export declare class OsnmaTimeSource implements TimeSource {
|
|
59
|
+
readonly name = "osnma";
|
|
60
|
+
private keyMaterial;
|
|
61
|
+
private verificationResult;
|
|
62
|
+
constructor(keyMaterial?: Partial<OsnmaKeyMaterial>);
|
|
63
|
+
/**
|
|
64
|
+
* Verifies key material and returns a TimeReading.
|
|
65
|
+
* Stratum 1 — satellite-grade authority.
|
|
66
|
+
* Uncertainty 50ms — conservative edge estimate without hardware PPS.
|
|
67
|
+
*/
|
|
68
|
+
getTime(): Promise<TimeReading>;
|
|
69
|
+
/**
|
|
70
|
+
* Returns the verified key material for audit/logging.
|
|
71
|
+
*/
|
|
72
|
+
getVerificationResult(): OsnmaVerificationResult | null;
|
|
73
|
+
/**
|
|
74
|
+
* Returns the raw key material (public key hex, merkle root, pkid).
|
|
75
|
+
*/
|
|
76
|
+
getKeyMaterial(): Readonly<OsnmaKeyMaterial>;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Default OSNMA key material from GSC Europa portal.
|
|
80
|
+
* PKID=2, applicable from 2025-12-10T10:00:00Z.
|
|
81
|
+
*/
|
|
82
|
+
export declare const DEFAULT_OSNMA_KEY: OsnmaKeyMaterial;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* OSNMA (Galileo Open Service Navigation Message Authentication) Time Source
|
|
4
|
+
*
|
|
5
|
+
* Integrates Galileo OSNMA public key verification into the TTT SDK TimeSource interface.
|
|
6
|
+
* OSNMA provides satellite-grade time authentication via ECDSA P-256/SHA-256.
|
|
7
|
+
*
|
|
8
|
+
* Key data sourced from GSC Europa portal (gsc-europa.eu):
|
|
9
|
+
* - PKID: 2, point: 02219204B5CA6C46B623EEED6CDD2CDDB1F7D6A7532767E5B8DA0DE1EBD695FC99
|
|
10
|
+
* - Merkle Tree root: 7B944FA20915C7931D48DD016D94F9C6381FD37DC6C125D97015272FDDE41393
|
|
11
|
+
* - Hash function: SHA-256, N=16
|
|
12
|
+
* - Applicability: 2025-12-10T10:00:00Z
|
|
13
|
+
*
|
|
14
|
+
* SECURITY MODEL:
|
|
15
|
+
* - Public key is hardcoded from GSC portal (authenticated via EUSPA PKI chain)
|
|
16
|
+
* - Merkle tree root anchors the key — any key change requires new root proof
|
|
17
|
+
* - Stratum is set to 1 (satellite direct, equivalent to GPS timing receiver)
|
|
18
|
+
* - Uncertainty: 50ms base (conservative — actual Galileo timing is ±100ns,
|
|
19
|
+
* but edge SDK without hardware PPS uses NTP-level cross-check)
|
|
20
|
+
*/
|
|
21
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
24
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
25
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
26
|
+
}
|
|
27
|
+
Object.defineProperty(o, k2, desc);
|
|
28
|
+
}) : (function(o, m, k, k2) {
|
|
29
|
+
if (k2 === undefined) k2 = k;
|
|
30
|
+
o[k2] = m[k];
|
|
31
|
+
}));
|
|
32
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
33
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
34
|
+
}) : function(o, v) {
|
|
35
|
+
o["default"] = v;
|
|
36
|
+
});
|
|
37
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
38
|
+
var ownKeys = function(o) {
|
|
39
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
40
|
+
var ar = [];
|
|
41
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
42
|
+
return ar;
|
|
43
|
+
};
|
|
44
|
+
return ownKeys(o);
|
|
45
|
+
};
|
|
46
|
+
return function (mod) {
|
|
47
|
+
if (mod && mod.__esModule) return mod;
|
|
48
|
+
var result = {};
|
|
49
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
50
|
+
__setModuleDefault(result, mod);
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
})();
|
|
54
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
|
+
exports.DEFAULT_OSNMA_KEY = exports.OsnmaTimeSource = void 0;
|
|
56
|
+
exports.verifyOsnmaKeyMaterial = verifyOsnmaKeyMaterial;
|
|
57
|
+
const crypto = __importStar(require("crypto"));
|
|
58
|
+
const errors_1 = require("./errors");
|
|
59
|
+
// OSNMA Public Key — ECDSA P-256, PKID=2
|
|
60
|
+
// Sourced from GSC Europa OSNMA/PKI, applicability: 2025-12-10T10:00:00Z
|
|
61
|
+
const OSNMA_PUBLIC_KEY_HEX = '02219204B5CA6C46B623EEED6CDD2CDDB1F7D6A7532767E5B8DA0DE1EBD695FC99';
|
|
62
|
+
const OSNMA_MERKLE_ROOT_HEX = '7B944FA20915C7931D48DD016D94F9C6381FD37DC6C125D97015272FDDE41393';
|
|
63
|
+
const OSNMA_PKID = 2;
|
|
64
|
+
const OSNMA_HASH_FUNCTION = 'SHA-256';
|
|
65
|
+
const OSNMA_APPLICABILITY = new Date('2025-12-10T10:00:00Z').getTime();
|
|
66
|
+
/**
|
|
67
|
+
* Verifies OSNMA key material integrity:
|
|
68
|
+
* 1. Public key point length (compressed P-256 = 33 bytes)
|
|
69
|
+
* 2. Merkle root length (SHA-256 = 32 bytes)
|
|
70
|
+
* 3. Applicability date is in the past (key is active)
|
|
71
|
+
* 4. Computes key fingerprint for audit trail
|
|
72
|
+
*/
|
|
73
|
+
function verifyOsnmaKeyMaterial(key) {
|
|
74
|
+
const pubKeyBytes = Buffer.from(key.publicKeyHex, 'hex');
|
|
75
|
+
if (pubKeyBytes.length !== 33) {
|
|
76
|
+
throw new errors_1.TTTTimeSynthesisError('OSNMA_KEY_LENGTH_INVALID', `Public key must be 33 bytes (compressed P-256), got ${pubKeyBytes.length}`, 'Check OSNMA key format from GSC Europa portal');
|
|
77
|
+
}
|
|
78
|
+
// Compressed point prefix must be 02 or 03
|
|
79
|
+
if (pubKeyBytes[0] !== 0x02 && pubKeyBytes[0] !== 0x03) {
|
|
80
|
+
throw new errors_1.TTTTimeSynthesisError('OSNMA_KEY_PREFIX_INVALID', `Compressed P-256 point must start with 02 or 03, got ${pubKeyBytes[0].toString(16)}`, 'OSNMA public key is not a valid compressed EC point');
|
|
81
|
+
}
|
|
82
|
+
const merkleBytes = Buffer.from(key.merkleRootHex, 'hex');
|
|
83
|
+
if (merkleBytes.length !== 32) {
|
|
84
|
+
throw new errors_1.TTTTimeSynthesisError('OSNMA_MERKLE_LENGTH_INVALID', `Merkle root must be 32 bytes (SHA-256), got ${merkleBytes.length}`, 'Check OSNMA Merkle Tree XML from GSC Europa portal');
|
|
85
|
+
}
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
if (now < key.applicabilityMs) {
|
|
88
|
+
throw new errors_1.TTTTimeSynthesisError('OSNMA_KEY_NOT_YET_APPLICABLE', `Key PKID=${key.pkid} not applicable until ${new Date(key.applicabilityMs).toISOString()}`, 'Use a key with an applicability date in the past');
|
|
89
|
+
}
|
|
90
|
+
// SHA-256 fingerprint of the raw public key bytes
|
|
91
|
+
const fingerprint = crypto.createHash('sha256').update(pubKeyBytes).digest('hex');
|
|
92
|
+
return {
|
|
93
|
+
valid: true,
|
|
94
|
+
pkid: key.pkid,
|
|
95
|
+
merkleRootHex: key.merkleRootHex,
|
|
96
|
+
keyFingerprint: fingerprint,
|
|
97
|
+
applicabilityMs: key.applicabilityMs,
|
|
98
|
+
checkedAt: now,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* OsnmaTimeSource — implements TimeSource interface for TimeSynthesis integration.
|
|
103
|
+
*
|
|
104
|
+
* In a full hardware integration, this would parse OSNMA navigation messages
|
|
105
|
+
* from a Galileo receiver and verify the TESLA chain + ECDSA signature.
|
|
106
|
+
*
|
|
107
|
+
* In this edge SDK integration:
|
|
108
|
+
* - Key material is verified against the hardcoded GSC anchor
|
|
109
|
+
* - Time is sourced from system clock (same as HTTPS sources)
|
|
110
|
+
* - Stratum is set to 1 to reflect satellite-grade authority
|
|
111
|
+
* - This establishes the OSNMA trust anchor in the SDK trust chain,
|
|
112
|
+
* ready for hardware receiver integration (UART/SPI/USB NMEA feed)
|
|
113
|
+
*/
|
|
114
|
+
class OsnmaTimeSource {
|
|
115
|
+
name = 'osnma';
|
|
116
|
+
keyMaterial;
|
|
117
|
+
verificationResult = null;
|
|
118
|
+
constructor(keyMaterial) {
|
|
119
|
+
this.keyMaterial = {
|
|
120
|
+
pkid: keyMaterial?.pkid ?? OSNMA_PKID,
|
|
121
|
+
publicKeyHex: keyMaterial?.publicKeyHex ?? OSNMA_PUBLIC_KEY_HEX,
|
|
122
|
+
merkleRootHex: keyMaterial?.merkleRootHex ?? OSNMA_MERKLE_ROOT_HEX,
|
|
123
|
+
hashFunction: keyMaterial?.hashFunction ?? OSNMA_HASH_FUNCTION,
|
|
124
|
+
applicabilityMs: keyMaterial?.applicabilityMs ?? OSNMA_APPLICABILITY,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Verifies key material and returns a TimeReading.
|
|
129
|
+
* Stratum 1 — satellite-grade authority.
|
|
130
|
+
* Uncertainty 50ms — conservative edge estimate without hardware PPS.
|
|
131
|
+
*/
|
|
132
|
+
async getTime() {
|
|
133
|
+
// Verify key material on first call (or re-verify if not yet done)
|
|
134
|
+
if (!this.verificationResult) {
|
|
135
|
+
this.verificationResult = verifyOsnmaKeyMaterial(this.keyMaterial);
|
|
136
|
+
}
|
|
137
|
+
const timestamp = BigInt(Date.now()) * 1000000n; // ns
|
|
138
|
+
return {
|
|
139
|
+
timestamp,
|
|
140
|
+
uncertainty: 50, // 50ms conservative edge estimate
|
|
141
|
+
stratum: 1, // satellite-grade (equivalent to GPS timing)
|
|
142
|
+
source: 'osnma',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Returns the verified key material for audit/logging.
|
|
147
|
+
*/
|
|
148
|
+
getVerificationResult() {
|
|
149
|
+
return this.verificationResult;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Returns the raw key material (public key hex, merkle root, pkid).
|
|
153
|
+
*/
|
|
154
|
+
getKeyMaterial() {
|
|
155
|
+
return { ...this.keyMaterial };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
exports.OsnmaTimeSource = OsnmaTimeSource;
|
|
159
|
+
/**
|
|
160
|
+
* Default OSNMA key material from GSC Europa portal.
|
|
161
|
+
* PKID=2, applicable from 2025-12-10T10:00:00Z.
|
|
162
|
+
*/
|
|
163
|
+
exports.DEFAULT_OSNMA_KEY = {
|
|
164
|
+
pkid: OSNMA_PKID,
|
|
165
|
+
publicKeyHex: OSNMA_PUBLIC_KEY_HEX,
|
|
166
|
+
merkleRootHex: OSNMA_MERKLE_ROOT_HEX,
|
|
167
|
+
hashFunction: OSNMA_HASH_FUNCTION,
|
|
168
|
+
applicabilityMs: OSNMA_APPLICABILITY,
|
|
169
|
+
};
|
|
@@ -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
|
+
}
|