@occ-core/stub 0.1.0
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/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/stub-host.d.ts +119 -0
- package/dist/stub-host.d.ts.map +1 -0
- package/dist/stub-host.js +212 -0
- package/dist/stub-host.js.map +1 -0
- package/package.json +58 -0
- package/src/index.ts +5 -0
- package/src/stub-host.ts +320 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @occ/stub — StubHost
|
|
3
|
+
*
|
|
4
|
+
* An in-process implementation of HostCapabilities for development and
|
|
5
|
+
* testing. All TEE-specific operations are performed in software using
|
|
6
|
+
* Node.js built-ins and @noble/ed25519.
|
|
7
|
+
*
|
|
8
|
+
* SECURITY WARNING: This implementation provides NO security guarantees.
|
|
9
|
+
* Keys are held in process memory (or a plaintext JSON file for persistence),
|
|
10
|
+
* the counter is software-only, and there is no hardware-backed attestation.
|
|
11
|
+
*
|
|
12
|
+
* DO NOT use StubHost in production or in any security-sensitive context.
|
|
13
|
+
* Its sole purpose is to allow developers to run, test, and explore
|
|
14
|
+
* occ-core locally without a real TEE.
|
|
15
|
+
*/
|
|
16
|
+
import type { HostCapabilities } from "@occ/core";
|
|
17
|
+
export interface StubHostOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Ed25519 private key (32 bytes).
|
|
20
|
+
* If omitted, a fresh random key is generated.
|
|
21
|
+
* Providing a fixed key is useful for deterministic tests.
|
|
22
|
+
*/
|
|
23
|
+
privateKey?: Uint8Array;
|
|
24
|
+
/**
|
|
25
|
+
* Measurement string returned by getMeasurement().
|
|
26
|
+
* Defaults to a fixed placeholder string.
|
|
27
|
+
*/
|
|
28
|
+
measurement?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Initial counter value. Defaults to 0.
|
|
31
|
+
*/
|
|
32
|
+
initialCounter?: bigint;
|
|
33
|
+
/**
|
|
34
|
+
* If true, secureTime() is available and returns Date.now().
|
|
35
|
+
* Defaults to true.
|
|
36
|
+
*/
|
|
37
|
+
enableTime?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* If true, nextCounter() is available.
|
|
40
|
+
* Defaults to true.
|
|
41
|
+
*/
|
|
42
|
+
enableCounter?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface PersistentStubHostOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Path to a JSON file where identity and counter state is persisted.
|
|
47
|
+
* If the file does not exist, it is created with a fresh keypair and
|
|
48
|
+
* counter starting at 0. If it exists, the stored key and counter
|
|
49
|
+
* are loaded and resumed.
|
|
50
|
+
*/
|
|
51
|
+
statePath: string;
|
|
52
|
+
/**
|
|
53
|
+
* Measurement string returned by getMeasurement().
|
|
54
|
+
* Defaults to "stub:measurement:not-a-real-tee".
|
|
55
|
+
*/
|
|
56
|
+
measurement?: string;
|
|
57
|
+
/**
|
|
58
|
+
* If true, secureTime() is available and returns Date.now().
|
|
59
|
+
* Defaults to true.
|
|
60
|
+
*/
|
|
61
|
+
enableTime?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* If true, nextCounter() is available and persists across restarts.
|
|
64
|
+
* Defaults to true.
|
|
65
|
+
*/
|
|
66
|
+
enableCounter?: boolean;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* StubHost wraps a HostCapabilities object built from in-process crypto.
|
|
70
|
+
*
|
|
71
|
+
* Optional capabilities are conditionally attached to the plain object
|
|
72
|
+
* returned by `.host` — this avoids TypeScript exactOptionalPropertyTypes
|
|
73
|
+
* conflicts that arise when implementing optional interface methods as class
|
|
74
|
+
* fields that could be undefined.
|
|
75
|
+
*/
|
|
76
|
+
export declare class StubHost {
|
|
77
|
+
#private;
|
|
78
|
+
private constructor();
|
|
79
|
+
/**
|
|
80
|
+
* Record the hash of the last produced proof into persistent state.
|
|
81
|
+
* Call this after each successful commit when using persistent mode to
|
|
82
|
+
* enable automatic proof chaining across restarts.
|
|
83
|
+
*/
|
|
84
|
+
setLastProofHash(hashB64: string): void;
|
|
85
|
+
/**
|
|
86
|
+
* Read the last proof hash from persistent state (for chaining).
|
|
87
|
+
* Returns undefined if no proof has been committed yet.
|
|
88
|
+
*/
|
|
89
|
+
getLastProofHash(): string | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Create a StubHost with ephemeral (in-memory) state.
|
|
92
|
+
* Counter resets to 0 on every process restart.
|
|
93
|
+
* Async because key derivation is async in @noble/ed25519.
|
|
94
|
+
*/
|
|
95
|
+
static create(opts?: StubHostOptions): Promise<StubHost>;
|
|
96
|
+
/**
|
|
97
|
+
* Create a StubHost that persists identity and counter to a JSON file.
|
|
98
|
+
*
|
|
99
|
+
* On first call: generates a fresh keypair and writes it to `statePath`.
|
|
100
|
+
* On subsequent calls: loads the existing keypair and counter and resumes.
|
|
101
|
+
*
|
|
102
|
+
* This lets a local demo service maintain a stable identity and
|
|
103
|
+
* monotonically increasing counter across restarts.
|
|
104
|
+
*
|
|
105
|
+
* The state file is plaintext JSON — not for production use.
|
|
106
|
+
*/
|
|
107
|
+
static createPersistent(opts: PersistentStubHostOptions): Promise<StubHost>;
|
|
108
|
+
/**
|
|
109
|
+
* Return the HostCapabilities object to pass to Constructor.initialize().
|
|
110
|
+
*/
|
|
111
|
+
get host(): HostCapabilities;
|
|
112
|
+
/** Return the raw private key bytes (test use only). */
|
|
113
|
+
get privateKeyBytes(): Uint8Array;
|
|
114
|
+
/** Return the raw public key bytes. */
|
|
115
|
+
get publicKeyBytes(): Uint8Array;
|
|
116
|
+
/** Return the current counter value without advancing it. */
|
|
117
|
+
get currentCounter(): bigint;
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=stub-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stub-host.d.ts","sourceRoot":"","sources":["../src/stub-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAMlD,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;IAExB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAMD,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAmBD;;;;;;;GAOG;AACH,qBAAa,QAAQ;;IAQnB,OAAO;IAoDP;;;;OAIG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAevC;;;OAGG;IACH,gBAAgB,IAAI,MAAM,GAAG,SAAS;IActC;;;;OAIG;WACU,MAAM,CAAC,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAkBlE;;;;;;;;;;OAUG;WACU,gBAAgB,CAAC,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAmDjF;;OAEG;IACH,IAAI,IAAI,IAAI,gBAAgB,CAE3B;IAED,wDAAwD;IACxD,IAAI,eAAe,IAAI,UAAU,CAEhC;IAED,uCAAuC;IACvC,IAAI,cAAc,IAAI,UAAU,CAE/B;IAED,6DAA6D;IAC7D,IAAI,cAAc,IAAI,MAAM,CAE3B;CACF"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @occ/stub — StubHost
|
|
3
|
+
*
|
|
4
|
+
* An in-process implementation of HostCapabilities for development and
|
|
5
|
+
* testing. All TEE-specific operations are performed in software using
|
|
6
|
+
* Node.js built-ins and @noble/ed25519.
|
|
7
|
+
*
|
|
8
|
+
* SECURITY WARNING: This implementation provides NO security guarantees.
|
|
9
|
+
* Keys are held in process memory (or a plaintext JSON file for persistence),
|
|
10
|
+
* the counter is software-only, and there is no hardware-backed attestation.
|
|
11
|
+
*
|
|
12
|
+
* DO NOT use StubHost in production or in any security-sensitive context.
|
|
13
|
+
* Its sole purpose is to allow developers to run, test, and explore
|
|
14
|
+
* occ-core locally without a real TEE.
|
|
15
|
+
*/
|
|
16
|
+
import { getPublicKeyAsync, signAsync, utils } from "@noble/ed25519";
|
|
17
|
+
import { randomBytes } from "crypto";
|
|
18
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
19
|
+
import { dirname } from "path";
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// StubHost
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/**
|
|
24
|
+
* StubHost wraps a HostCapabilities object built from in-process crypto.
|
|
25
|
+
*
|
|
26
|
+
* Optional capabilities are conditionally attached to the plain object
|
|
27
|
+
* returned by `.host` — this avoids TypeScript exactOptionalPropertyTypes
|
|
28
|
+
* conflicts that arise when implementing optional interface methods as class
|
|
29
|
+
* fields that could be undefined.
|
|
30
|
+
*/
|
|
31
|
+
export class StubHost {
|
|
32
|
+
#host;
|
|
33
|
+
#privateKey;
|
|
34
|
+
#publicKey;
|
|
35
|
+
#measurement;
|
|
36
|
+
#counter;
|
|
37
|
+
#statePath;
|
|
38
|
+
constructor(privateKey, publicKey, opts, statePath) {
|
|
39
|
+
this.#privateKey = privateKey;
|
|
40
|
+
this.#publicKey = publicKey;
|
|
41
|
+
this.#measurement = opts.measurement;
|
|
42
|
+
this.#counter = opts.initialCounter;
|
|
43
|
+
this.#statePath = statePath;
|
|
44
|
+
// Build the base host with required capabilities
|
|
45
|
+
const base = {
|
|
46
|
+
enforcementTier: "stub",
|
|
47
|
+
getMeasurement: async () => this.#measurement,
|
|
48
|
+
getFreshNonce: async () => new Uint8Array(randomBytes(32)),
|
|
49
|
+
sign: async (data) => signAsync(data, this.#privateKey),
|
|
50
|
+
getPublicKey: async () => this.#publicKey,
|
|
51
|
+
};
|
|
52
|
+
// Conditionally attach optional capabilities
|
|
53
|
+
if (opts.enableCounter) {
|
|
54
|
+
base.nextCounter = async () => {
|
|
55
|
+
this.#counter += 1n;
|
|
56
|
+
if (this.#statePath !== undefined) {
|
|
57
|
+
this.#persistState();
|
|
58
|
+
}
|
|
59
|
+
return String(this.#counter);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (opts.enableTime) {
|
|
63
|
+
base.secureTime = async () => Date.now();
|
|
64
|
+
}
|
|
65
|
+
this.#host = base;
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Persistence helpers
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
#persistState() {
|
|
71
|
+
if (this.#statePath === undefined)
|
|
72
|
+
return;
|
|
73
|
+
const state = {
|
|
74
|
+
privateKeyB64: Buffer.from(this.#privateKey).toString("base64"),
|
|
75
|
+
counter: String(this.#counter),
|
|
76
|
+
};
|
|
77
|
+
writeFileSync(this.#statePath, JSON.stringify(state, null, 2), "utf8");
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Record the hash of the last produced proof into persistent state.
|
|
81
|
+
* Call this after each successful commit when using persistent mode to
|
|
82
|
+
* enable automatic proof chaining across restarts.
|
|
83
|
+
*/
|
|
84
|
+
setLastProofHash(hashB64) {
|
|
85
|
+
if (this.#statePath === undefined)
|
|
86
|
+
return;
|
|
87
|
+
let existing;
|
|
88
|
+
try {
|
|
89
|
+
existing = JSON.parse(readFileSync(this.#statePath, "utf8"));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
existing = {
|
|
93
|
+
privateKeyB64: Buffer.from(this.#privateKey).toString("base64"),
|
|
94
|
+
counter: String(this.#counter),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
existing.lastProofHashB64 = hashB64;
|
|
98
|
+
writeFileSync(this.#statePath, JSON.stringify(existing, null, 2), "utf8");
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Read the last proof hash from persistent state (for chaining).
|
|
102
|
+
* Returns undefined if no proof has been committed yet.
|
|
103
|
+
*/
|
|
104
|
+
getLastProofHash() {
|
|
105
|
+
if (this.#statePath === undefined)
|
|
106
|
+
return undefined;
|
|
107
|
+
try {
|
|
108
|
+
const raw = JSON.parse(readFileSync(this.#statePath, "utf8"));
|
|
109
|
+
return raw.lastProofHashB64;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Factory methods
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
/**
|
|
119
|
+
* Create a StubHost with ephemeral (in-memory) state.
|
|
120
|
+
* Counter resets to 0 on every process restart.
|
|
121
|
+
* Async because key derivation is async in @noble/ed25519.
|
|
122
|
+
*/
|
|
123
|
+
static async create(opts = {}) {
|
|
124
|
+
const privateKey = opts.privateKey ?? utils.randomPrivateKey();
|
|
125
|
+
if (privateKey.length !== 32) {
|
|
126
|
+
throw new RangeError("StubHost: privateKey must be 32 bytes");
|
|
127
|
+
}
|
|
128
|
+
const publicKey = await getPublicKeyAsync(privateKey);
|
|
129
|
+
const resolved = {
|
|
130
|
+
privateKey,
|
|
131
|
+
measurement: opts.measurement ?? "stub:measurement:not-a-real-tee",
|
|
132
|
+
initialCounter: opts.initialCounter ?? 0n,
|
|
133
|
+
enableTime: opts.enableTime ?? true,
|
|
134
|
+
enableCounter: opts.enableCounter ?? true,
|
|
135
|
+
};
|
|
136
|
+
return new StubHost(privateKey, publicKey, resolved);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Create a StubHost that persists identity and counter to a JSON file.
|
|
140
|
+
*
|
|
141
|
+
* On first call: generates a fresh keypair and writes it to `statePath`.
|
|
142
|
+
* On subsequent calls: loads the existing keypair and counter and resumes.
|
|
143
|
+
*
|
|
144
|
+
* This lets a local demo service maintain a stable identity and
|
|
145
|
+
* monotonically increasing counter across restarts.
|
|
146
|
+
*
|
|
147
|
+
* The state file is plaintext JSON — not for production use.
|
|
148
|
+
*/
|
|
149
|
+
static async createPersistent(opts) {
|
|
150
|
+
const { statePath, measurement, enableTime, enableCounter } = opts;
|
|
151
|
+
let privateKey;
|
|
152
|
+
let initialCounter;
|
|
153
|
+
// Try to load existing state
|
|
154
|
+
let existingState;
|
|
155
|
+
try {
|
|
156
|
+
existingState = JSON.parse(readFileSync(statePath, "utf8"));
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// File doesn't exist or is corrupt — start fresh
|
|
160
|
+
}
|
|
161
|
+
if (existingState !== undefined) {
|
|
162
|
+
privateKey = new Uint8Array(Buffer.from(existingState.privateKeyB64, "base64"));
|
|
163
|
+
initialCounter = BigInt(existingState.counter);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
// First run — generate a fresh key
|
|
167
|
+
privateKey = utils.randomPrivateKey();
|
|
168
|
+
initialCounter = 0n;
|
|
169
|
+
// Ensure the directory exists
|
|
170
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
171
|
+
const initialState = {
|
|
172
|
+
privateKeyB64: Buffer.from(privateKey).toString("base64"),
|
|
173
|
+
counter: String(initialCounter),
|
|
174
|
+
};
|
|
175
|
+
writeFileSync(statePath, JSON.stringify(initialState, null, 2), "utf8");
|
|
176
|
+
}
|
|
177
|
+
if (privateKey.length !== 32) {
|
|
178
|
+
throw new RangeError("StubHost.createPersistent: loaded key is not 32 bytes");
|
|
179
|
+
}
|
|
180
|
+
const publicKey = await getPublicKeyAsync(privateKey);
|
|
181
|
+
const resolved = {
|
|
182
|
+
privateKey,
|
|
183
|
+
measurement: measurement ?? "stub:measurement:not-a-real-tee",
|
|
184
|
+
initialCounter,
|
|
185
|
+
enableTime: enableTime ?? true,
|
|
186
|
+
enableCounter: enableCounter ?? true,
|
|
187
|
+
};
|
|
188
|
+
return new StubHost(privateKey, publicKey, resolved, statePath);
|
|
189
|
+
}
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Accessors
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
/**
|
|
194
|
+
* Return the HostCapabilities object to pass to Constructor.initialize().
|
|
195
|
+
*/
|
|
196
|
+
get host() {
|
|
197
|
+
return this.#host;
|
|
198
|
+
}
|
|
199
|
+
/** Return the raw private key bytes (test use only). */
|
|
200
|
+
get privateKeyBytes() {
|
|
201
|
+
return this.#privateKey;
|
|
202
|
+
}
|
|
203
|
+
/** Return the raw public key bytes. */
|
|
204
|
+
get publicKeyBytes() {
|
|
205
|
+
return this.#publicKey;
|
|
206
|
+
}
|
|
207
|
+
/** Return the current counter value without advancing it. */
|
|
208
|
+
get currentCounter() {
|
|
209
|
+
return this.#counter;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=stub-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stub-host.js","sourceRoot":"","sources":["../src/stub-host.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@occ-core/stub",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "In-process HostCapabilities stub for occ-core — development and testing only",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"occ",
|
|
7
|
+
"origin-controlled-computing",
|
|
8
|
+
"tee",
|
|
9
|
+
"stub",
|
|
10
|
+
"dev",
|
|
11
|
+
"ed25519",
|
|
12
|
+
"tamper-evident"
|
|
13
|
+
],
|
|
14
|
+
"author": "Mike Argento",
|
|
15
|
+
"license": "Apache-2.0",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/mikeargento/occ-core.git",
|
|
19
|
+
"directory": "packages/occ-core-stub"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/mikeargento/occ-core#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/mikeargento/occ-core/issues"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"!dist/__tests__",
|
|
38
|
+
"src",
|
|
39
|
+
"!src/__tests__"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"test": "npm run build && node --test dist/__tests__/*.test.js",
|
|
45
|
+
"prepublishOnly": "npm run build && npm test"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@noble/ed25519": "^2.1.0",
|
|
49
|
+
"@occ-core/core": "*"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^20.0.0",
|
|
53
|
+
"typescript": "^5.4.0"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=18.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/index.ts
ADDED
package/src/stub-host.ts
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @occ/stub — StubHost
|
|
3
|
+
*
|
|
4
|
+
* An in-process implementation of HostCapabilities for development and
|
|
5
|
+
* testing. All TEE-specific operations are performed in software using
|
|
6
|
+
* Node.js built-ins and @noble/ed25519.
|
|
7
|
+
*
|
|
8
|
+
* SECURITY WARNING: This implementation provides NO security guarantees.
|
|
9
|
+
* Keys are held in process memory (or a plaintext JSON file for persistence),
|
|
10
|
+
* the counter is software-only, and there is no hardware-backed attestation.
|
|
11
|
+
*
|
|
12
|
+
* DO NOT use StubHost in production or in any security-sensitive context.
|
|
13
|
+
* Its sole purpose is to allow developers to run, test, and explore
|
|
14
|
+
* occ-core locally without a real TEE.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { getPublicKeyAsync, signAsync, utils } from "@noble/ed25519";
|
|
18
|
+
import { randomBytes } from "crypto";
|
|
19
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
20
|
+
import { dirname } from "path";
|
|
21
|
+
import type { HostCapabilities } from "@occ/core";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// StubHost options
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export interface StubHostOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Ed25519 private key (32 bytes).
|
|
30
|
+
* If omitted, a fresh random key is generated.
|
|
31
|
+
* Providing a fixed key is useful for deterministic tests.
|
|
32
|
+
*/
|
|
33
|
+
privateKey?: Uint8Array;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Measurement string returned by getMeasurement().
|
|
37
|
+
* Defaults to a fixed placeholder string.
|
|
38
|
+
*/
|
|
39
|
+
measurement?: string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Initial counter value. Defaults to 0.
|
|
43
|
+
*/
|
|
44
|
+
initialCounter?: bigint;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* If true, secureTime() is available and returns Date.now().
|
|
48
|
+
* Defaults to true.
|
|
49
|
+
*/
|
|
50
|
+
enableTime?: boolean;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* If true, nextCounter() is available.
|
|
54
|
+
* Defaults to true.
|
|
55
|
+
*/
|
|
56
|
+
enableCounter?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Persistent StubHost options
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
export interface PersistentStubHostOptions {
|
|
64
|
+
/**
|
|
65
|
+
* Path to a JSON file where identity and counter state is persisted.
|
|
66
|
+
* If the file does not exist, it is created with a fresh keypair and
|
|
67
|
+
* counter starting at 0. If it exists, the stored key and counter
|
|
68
|
+
* are loaded and resumed.
|
|
69
|
+
*/
|
|
70
|
+
statePath: string;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Measurement string returned by getMeasurement().
|
|
74
|
+
* Defaults to "stub:measurement:not-a-real-tee".
|
|
75
|
+
*/
|
|
76
|
+
measurement?: string;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* If true, secureTime() is available and returns Date.now().
|
|
80
|
+
* Defaults to true.
|
|
81
|
+
*/
|
|
82
|
+
enableTime?: boolean;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* If true, nextCounter() is available and persists across restarts.
|
|
86
|
+
* Defaults to true.
|
|
87
|
+
*/
|
|
88
|
+
enableCounter?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Persisted state shape (written as JSON)
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
interface PersistedState {
|
|
96
|
+
/** Base64-encoded Ed25519 private key (32 bytes). */
|
|
97
|
+
privateKeyB64: string;
|
|
98
|
+
/** Current counter value as a decimal string (survives BigInt round-trip). */
|
|
99
|
+
counter: string;
|
|
100
|
+
/** Base64 of the last proof's canonical hash — populated by the service layer. */
|
|
101
|
+
lastProofHashB64?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// StubHost
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* StubHost wraps a HostCapabilities object built from in-process crypto.
|
|
110
|
+
*
|
|
111
|
+
* Optional capabilities are conditionally attached to the plain object
|
|
112
|
+
* returned by `.host` — this avoids TypeScript exactOptionalPropertyTypes
|
|
113
|
+
* conflicts that arise when implementing optional interface methods as class
|
|
114
|
+
* fields that could be undefined.
|
|
115
|
+
*/
|
|
116
|
+
export class StubHost {
|
|
117
|
+
readonly #host: HostCapabilities;
|
|
118
|
+
readonly #privateKey: Uint8Array;
|
|
119
|
+
readonly #publicKey: Uint8Array;
|
|
120
|
+
readonly #measurement: string;
|
|
121
|
+
#counter: bigint;
|
|
122
|
+
readonly #statePath: string | undefined;
|
|
123
|
+
|
|
124
|
+
private constructor(
|
|
125
|
+
privateKey: Uint8Array,
|
|
126
|
+
publicKey: Uint8Array,
|
|
127
|
+
opts: Required<StubHostOptions>,
|
|
128
|
+
statePath?: string
|
|
129
|
+
) {
|
|
130
|
+
this.#privateKey = privateKey;
|
|
131
|
+
this.#publicKey = publicKey;
|
|
132
|
+
this.#measurement = opts.measurement;
|
|
133
|
+
this.#counter = opts.initialCounter;
|
|
134
|
+
this.#statePath = statePath;
|
|
135
|
+
|
|
136
|
+
// Build the base host with required capabilities
|
|
137
|
+
const base: HostCapabilities = {
|
|
138
|
+
enforcementTier: "stub",
|
|
139
|
+
getMeasurement: async () => this.#measurement,
|
|
140
|
+
getFreshNonce: async () => new Uint8Array(randomBytes(32)),
|
|
141
|
+
sign: async (data: Uint8Array) => signAsync(data, this.#privateKey),
|
|
142
|
+
getPublicKey: async () => this.#publicKey,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Conditionally attach optional capabilities
|
|
146
|
+
if (opts.enableCounter) {
|
|
147
|
+
base.nextCounter = async (): Promise<string> => {
|
|
148
|
+
this.#counter += 1n;
|
|
149
|
+
if (this.#statePath !== undefined) {
|
|
150
|
+
this.#persistState();
|
|
151
|
+
}
|
|
152
|
+
return String(this.#counter);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (opts.enableTime) {
|
|
157
|
+
base.secureTime = async (): Promise<number> => Date.now();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.#host = base;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Persistence helpers
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
#persistState(): void {
|
|
168
|
+
if (this.#statePath === undefined) return;
|
|
169
|
+
const state: PersistedState = {
|
|
170
|
+
privateKeyB64: Buffer.from(this.#privateKey).toString("base64"),
|
|
171
|
+
counter: String(this.#counter),
|
|
172
|
+
};
|
|
173
|
+
writeFileSync(this.#statePath, JSON.stringify(state, null, 2), "utf8");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Record the hash of the last produced proof into persistent state.
|
|
178
|
+
* Call this after each successful commit when using persistent mode to
|
|
179
|
+
* enable automatic proof chaining across restarts.
|
|
180
|
+
*/
|
|
181
|
+
setLastProofHash(hashB64: string): void {
|
|
182
|
+
if (this.#statePath === undefined) return;
|
|
183
|
+
let existing: PersistedState;
|
|
184
|
+
try {
|
|
185
|
+
existing = JSON.parse(readFileSync(this.#statePath, "utf8")) as PersistedState;
|
|
186
|
+
} catch {
|
|
187
|
+
existing = {
|
|
188
|
+
privateKeyB64: Buffer.from(this.#privateKey).toString("base64"),
|
|
189
|
+
counter: String(this.#counter),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
existing.lastProofHashB64 = hashB64;
|
|
193
|
+
writeFileSync(this.#statePath, JSON.stringify(existing, null, 2), "utf8");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Read the last proof hash from persistent state (for chaining).
|
|
198
|
+
* Returns undefined if no proof has been committed yet.
|
|
199
|
+
*/
|
|
200
|
+
getLastProofHash(): string | undefined {
|
|
201
|
+
if (this.#statePath === undefined) return undefined;
|
|
202
|
+
try {
|
|
203
|
+
const raw = JSON.parse(readFileSync(this.#statePath, "utf8")) as PersistedState;
|
|
204
|
+
return raw.lastProofHashB64;
|
|
205
|
+
} catch {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Factory methods
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Create a StubHost with ephemeral (in-memory) state.
|
|
216
|
+
* Counter resets to 0 on every process restart.
|
|
217
|
+
* Async because key derivation is async in @noble/ed25519.
|
|
218
|
+
*/
|
|
219
|
+
static async create(opts: StubHostOptions = {}): Promise<StubHost> {
|
|
220
|
+
const privateKey = opts.privateKey ?? utils.randomPrivateKey();
|
|
221
|
+
if (privateKey.length !== 32) {
|
|
222
|
+
throw new RangeError("StubHost: privateKey must be 32 bytes");
|
|
223
|
+
}
|
|
224
|
+
const publicKey = await getPublicKeyAsync(privateKey);
|
|
225
|
+
|
|
226
|
+
const resolved: Required<StubHostOptions> = {
|
|
227
|
+
privateKey,
|
|
228
|
+
measurement: opts.measurement ?? "stub:measurement:not-a-real-tee",
|
|
229
|
+
initialCounter: opts.initialCounter ?? 0n,
|
|
230
|
+
enableTime: opts.enableTime ?? true,
|
|
231
|
+
enableCounter: opts.enableCounter ?? true,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return new StubHost(privateKey, publicKey, resolved);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Create a StubHost that persists identity and counter to a JSON file.
|
|
239
|
+
*
|
|
240
|
+
* On first call: generates a fresh keypair and writes it to `statePath`.
|
|
241
|
+
* On subsequent calls: loads the existing keypair and counter and resumes.
|
|
242
|
+
*
|
|
243
|
+
* This lets a local demo service maintain a stable identity and
|
|
244
|
+
* monotonically increasing counter across restarts.
|
|
245
|
+
*
|
|
246
|
+
* The state file is plaintext JSON — not for production use.
|
|
247
|
+
*/
|
|
248
|
+
static async createPersistent(opts: PersistentStubHostOptions): Promise<StubHost> {
|
|
249
|
+
const { statePath, measurement, enableTime, enableCounter } = opts;
|
|
250
|
+
|
|
251
|
+
let privateKey: Uint8Array;
|
|
252
|
+
let initialCounter: bigint;
|
|
253
|
+
|
|
254
|
+
// Try to load existing state
|
|
255
|
+
let existingState: PersistedState | undefined;
|
|
256
|
+
try {
|
|
257
|
+
existingState = JSON.parse(readFileSync(statePath, "utf8")) as PersistedState;
|
|
258
|
+
} catch {
|
|
259
|
+
// File doesn't exist or is corrupt — start fresh
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (existingState !== undefined) {
|
|
263
|
+
privateKey = new Uint8Array(Buffer.from(existingState.privateKeyB64, "base64"));
|
|
264
|
+
initialCounter = BigInt(existingState.counter);
|
|
265
|
+
} else {
|
|
266
|
+
// First run — generate a fresh key
|
|
267
|
+
privateKey = utils.randomPrivateKey();
|
|
268
|
+
initialCounter = 0n;
|
|
269
|
+
// Ensure the directory exists
|
|
270
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
271
|
+
const initialState: PersistedState = {
|
|
272
|
+
privateKeyB64: Buffer.from(privateKey).toString("base64"),
|
|
273
|
+
counter: String(initialCounter),
|
|
274
|
+
};
|
|
275
|
+
writeFileSync(statePath, JSON.stringify(initialState, null, 2), "utf8");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (privateKey.length !== 32) {
|
|
279
|
+
throw new RangeError("StubHost.createPersistent: loaded key is not 32 bytes");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const publicKey = await getPublicKeyAsync(privateKey);
|
|
283
|
+
|
|
284
|
+
const resolved: Required<StubHostOptions> = {
|
|
285
|
+
privateKey,
|
|
286
|
+
measurement: measurement ?? "stub:measurement:not-a-real-tee",
|
|
287
|
+
initialCounter,
|
|
288
|
+
enableTime: enableTime ?? true,
|
|
289
|
+
enableCounter: enableCounter ?? true,
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
return new StubHost(privateKey, publicKey, resolved, statePath);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Accessors
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Return the HostCapabilities object to pass to Constructor.initialize().
|
|
301
|
+
*/
|
|
302
|
+
get host(): HostCapabilities {
|
|
303
|
+
return this.#host;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Return the raw private key bytes (test use only). */
|
|
307
|
+
get privateKeyBytes(): Uint8Array {
|
|
308
|
+
return this.#privateKey;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Return the raw public key bytes. */
|
|
312
|
+
get publicKeyBytes(): Uint8Array {
|
|
313
|
+
return this.#publicKey;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Return the current counter value without advancing it. */
|
|
317
|
+
get currentCounter(): bigint {
|
|
318
|
+
return this.#counter;
|
|
319
|
+
}
|
|
320
|
+
}
|