@opendatalabs/vana-sdk 3.22.0 → 3.23.0-pr.211.341f55d
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/crypto/envelope/job.cjs +355 -0
- package/dist/crypto/envelope/job.cjs.map +1 -0
- package/dist/crypto/envelope/job.d.ts +123 -0
- package/dist/crypto/envelope/job.js +333 -0
- package/dist/crypto/envelope/job.js.map +1 -0
- package/dist/direct/access-request-client.cjs +4 -0
- package/dist/direct/access-request-client.cjs.map +1 -1
- package/dist/direct/access-request-client.js +4 -0
- package/dist/direct/access-request-client.js.map +1 -1
- package/dist/direct/connect-flow.cjs +43 -1
- package/dist/direct/connect-flow.cjs.map +1 -1
- package/dist/direct/connect-flow.d.ts +12 -0
- package/dist/direct/connect-flow.js +43 -1
- package/dist/direct/connect-flow.js.map +1 -1
- package/dist/direct/types.cjs.map +1 -1
- package/dist/direct/types.d.ts +7 -0
- package/dist/direct/types.js.map +1 -1
- package/dist/direct/use-direct-vana-connect.cjs +2 -1
- package/dist/direct/use-direct-vana-connect.cjs.map +1 -1
- package/dist/direct/use-direct-vana-connect.d.ts +3 -1
- package/dist/direct/use-direct-vana-connect.js +2 -1
- package/dist/direct/use-direct-vana-connect.js.map +1 -1
- package/dist/errors.cjs +84 -0
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +133 -0
- package/dist/errors.js +73 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.browser.d.ts +3 -0
- package/dist/index.browser.js +631 -24
- package/dist/index.browser.js.map +4 -4
- package/dist/index.node.cjs +1259 -111
- package/dist/index.node.cjs.map +4 -4
- package/dist/index.node.d.ts +4 -0
- package/dist/index.node.js +1223 -101
- package/dist/index.node.js.map +4 -4
- package/dist/protocol/identity.cjs +219 -0
- package/dist/protocol/identity.cjs.map +1 -0
- package/dist/protocol/identity.d.ts +193 -0
- package/dist/protocol/identity.js +183 -0
- package/dist/protocol/identity.js.map +1 -0
- package/dist/protocol/identity.test.d.ts +1 -0
- package/dist/protocol/identity.vector.test.d.ts +1 -0
- package/dist/protocol/jobs-client.cjs +540 -0
- package/dist/protocol/jobs-client.cjs.map +1 -0
- package/dist/protocol/jobs-client.d.ts +169 -0
- package/dist/protocol/jobs-client.js +547 -0
- package/dist/protocol/jobs-client.js.map +1 -0
- package/dist/protocol/jobs-client.test.d.ts +1 -0
- package/dist/protocol/jobs.cjs +64 -0
- package/dist/protocol/jobs.cjs.map +1 -0
- package/dist/protocol/jobs.d.ts +193 -0
- package/dist/protocol/jobs.js +31 -0
- package/dist/protocol/jobs.js.map +1 -0
- package/dist/protocol/jobs.test.d.ts +1 -0
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js.map +1 -1
- package/package.json +7 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var identity_exports = {};
|
|
30
|
+
__export(identity_exports, {
|
|
31
|
+
ENCLAVE_IDENTITY_EVIDENCE_VERSION: () => ENCLAVE_IDENTITY_EVIDENCE_VERSION,
|
|
32
|
+
ENCLAVE_TRUST_ANCHORS: () => ENCLAVE_TRUST_ANCHORS,
|
|
33
|
+
ENCLAVE_WALLET_PURPOSE: () => ENCLAVE_WALLET_PURPOSE,
|
|
34
|
+
MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS: () => MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS,
|
|
35
|
+
MASTER_SIGNATURE_DELIVERY_VERSION: () => MASTER_SIGNATURE_DELIVERY_VERSION,
|
|
36
|
+
SEALED_ENVELOPE_VERSION: () => SEALED_ENVELOPE_VERSION,
|
|
37
|
+
USER_PS_ID_DOMAIN: () => USER_PS_ID_DOMAIN,
|
|
38
|
+
appRootPreimage: () => appRootPreimage,
|
|
39
|
+
buildMasterSignatureDelivery: () => buildMasterSignatureDelivery,
|
|
40
|
+
encryptMasterSignatureDelivery: () => encryptMasterSignatureDelivery,
|
|
41
|
+
kmsIssuedPreimage: () => kmsIssuedPreimage,
|
|
42
|
+
userPsId: () => userPsId,
|
|
43
|
+
verifyEnclaveIdentityEvidence: () => verifyEnclaveIdentityEvidence
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(identity_exports);
|
|
46
|
+
var secp256k1 = __toESM(require("@noble/secp256k1"), 1);
|
|
47
|
+
var import_viem = require("viem");
|
|
48
|
+
var import_accounts = require("viem/accounts");
|
|
49
|
+
var import_interface = require("../crypto/ecies/interface");
|
|
50
|
+
var import_derive = require("../crypto/keys/derive");
|
|
51
|
+
const ENCLAVE_IDENTITY_EVIDENCE_VERSION = 1;
|
|
52
|
+
const USER_PS_ID_DOMAIN = "vana.ps-enclave.v1";
|
|
53
|
+
const ENCLAVE_WALLET_PURPOSE = "vana.ps-enclave.wallet.v1";
|
|
54
|
+
const MASTER_SIGNATURE_DELIVERY_VERSION = "vana.ps-enclave.delivery.v1";
|
|
55
|
+
const MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS = 600;
|
|
56
|
+
const SEALED_ENVELOPE_VERSION = 1;
|
|
57
|
+
const VANA_MAINNET_CHAIN_ID = 1480;
|
|
58
|
+
const MOKSHA_CHAIN_ID = 14800;
|
|
59
|
+
const KMS_ISSUED_PREFIX = "dstack-kms-issued";
|
|
60
|
+
const PREIMAGE_SEPARATOR = ":";
|
|
61
|
+
const UNCOMPRESSED_PUBLIC_KEY_BYTES = 65;
|
|
62
|
+
const UNCOMPRESSED_PUBLIC_KEY_PREFIX = "04";
|
|
63
|
+
const EMPTY_HEX = "0x";
|
|
64
|
+
function emptyAnchor() {
|
|
65
|
+
return Object.freeze({
|
|
66
|
+
kmsRootPubkey: EMPTY_HEX,
|
|
67
|
+
appIds: Object.freeze([])
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const ENCLAVE_TRUST_ANCHORS = Object.freeze({
|
|
71
|
+
// filled at fleet provisioning; verify fails closed while empty
|
|
72
|
+
[VANA_MAINNET_CHAIN_ID]: emptyAnchor(),
|
|
73
|
+
// filled at fleet provisioning; verify fails closed while empty
|
|
74
|
+
[MOKSHA_CHAIN_ID]: emptyAnchor()
|
|
75
|
+
});
|
|
76
|
+
function userPsId(chainId, ownerAddress) {
|
|
77
|
+
const packed = (0, import_viem.encodePacked)(
|
|
78
|
+
["string", "uint256", "address"],
|
|
79
|
+
[USER_PS_ID_DOMAIN, BigInt(chainId), (0, import_viem.getAddress)(ownerAddress)]
|
|
80
|
+
);
|
|
81
|
+
return (0, import_viem.keccak256)(packed);
|
|
82
|
+
}
|
|
83
|
+
function compressPublicKey(publicKey) {
|
|
84
|
+
return secp256k1.ProjectivePoint.fromHex(
|
|
85
|
+
(0, import_viem.fromHex)(publicKey, "bytes")
|
|
86
|
+
).toRawBytes(true);
|
|
87
|
+
}
|
|
88
|
+
function sameKey(a, b) {
|
|
89
|
+
return (0, import_viem.toHex)(compressPublicKey(a)) === (0, import_viem.toHex)(compressPublicKey(b));
|
|
90
|
+
}
|
|
91
|
+
function assertUncompressedKey(publicKey) {
|
|
92
|
+
const hex = publicKey.slice(2);
|
|
93
|
+
if (hex.length !== UNCOMPRESSED_PUBLIC_KEY_BYTES * 2 || !hex.startsWith(UNCOMPRESSED_PUBLIC_KEY_PREFIX) || !/^[0-9a-fA-F]+$/.test(hex)) {
|
|
94
|
+
throw new Error("Public key must be a 65-byte uncompressed secp256k1 key");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function appRootPreimage(purpose, publicKey) {
|
|
98
|
+
const keyHex = (0, import_viem.toHex)(compressPublicKey(publicKey)).slice(2);
|
|
99
|
+
return (0, import_viem.keccak256)((0, import_viem.toBytes)(`${purpose}${PREIMAGE_SEPARATOR}${keyHex}`));
|
|
100
|
+
}
|
|
101
|
+
function kmsIssuedPreimage(appId, appRootPublicKey) {
|
|
102
|
+
const prefix = (0, import_viem.concat)([
|
|
103
|
+
(0, import_viem.toBytes)(`${KMS_ISSUED_PREFIX}${PREIMAGE_SEPARATOR}`),
|
|
104
|
+
(0, import_viem.fromHex)(appId, "bytes")
|
|
105
|
+
]);
|
|
106
|
+
const compressed = compressPublicKey(appRootPublicKey);
|
|
107
|
+
return (0, import_viem.keccak256)((0, import_viem.concat)([prefix, compressed]));
|
|
108
|
+
}
|
|
109
|
+
async function recoverChainKey(hash, signature, link) {
|
|
110
|
+
try {
|
|
111
|
+
return (await (0, import_viem.recoverPublicKey)({ hash, signature })).toLowerCase();
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error(`Invalid enclave signature chain link ${link}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function verifyEnclaveIdentityEvidence(evidence, anchors, expected) {
|
|
117
|
+
if (evidence.v !== ENCLAVE_IDENTITY_EVIDENCE_VERSION) {
|
|
118
|
+
throw new Error("Unsupported enclave identity evidence version");
|
|
119
|
+
}
|
|
120
|
+
if (!Number.isInteger(evidence.epoch) || evidence.epoch < 1) {
|
|
121
|
+
throw new Error("Invalid enclave identity epoch");
|
|
122
|
+
}
|
|
123
|
+
if (evidence.chainId !== expected.chainId) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
"Enclave identity chain ID does not match expected chain ID"
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (!(0, import_viem.isAddressEqual)(evidence.ownerAddress, expected.ownerAddress)) {
|
|
129
|
+
throw new Error("Enclave identity owner does not match expected owner");
|
|
130
|
+
}
|
|
131
|
+
if (evidence.epoch !== expected.epoch) {
|
|
132
|
+
throw new Error("Enclave identity epoch does not match expected epoch");
|
|
133
|
+
}
|
|
134
|
+
const expectedUserPsId = userPsId(expected.chainId, expected.ownerAddress);
|
|
135
|
+
if (evidence.userPsId.toLowerCase() !== expectedUserPsId.toLowerCase()) {
|
|
136
|
+
throw new Error("Enclave userPsId does not match expected identity");
|
|
137
|
+
}
|
|
138
|
+
if (evidence.purpose !== ENCLAVE_WALLET_PURPOSE) {
|
|
139
|
+
throw new Error("Unexpected enclave wallet purpose");
|
|
140
|
+
}
|
|
141
|
+
if (anchors.kmsRootPubkey === EMPTY_HEX) {
|
|
142
|
+
throw new Error("KMS root trust anchor is not provisioned");
|
|
143
|
+
}
|
|
144
|
+
assertUncompressedKey(evidence.publicKey);
|
|
145
|
+
const derivedAddress = (0, import_accounts.publicKeyToAddress)(evidence.publicKey);
|
|
146
|
+
if ((0, import_viem.getAddress)(derivedAddress) !== (0, import_viem.getAddress)(evidence.address)) {
|
|
147
|
+
throw new Error("Enclave public key does not match its address");
|
|
148
|
+
}
|
|
149
|
+
const appRootPublicKey = await recoverChainKey(
|
|
150
|
+
appRootPreimage(evidence.purpose, evidence.publicKey),
|
|
151
|
+
evidence.signatureChain[0],
|
|
152
|
+
0
|
|
153
|
+
);
|
|
154
|
+
const kmsRootPublicKey = await recoverChainKey(
|
|
155
|
+
kmsIssuedPreimage(evidence.appId, appRootPublicKey),
|
|
156
|
+
evidence.signatureChain[1],
|
|
157
|
+
1
|
|
158
|
+
);
|
|
159
|
+
let matchesAnchor;
|
|
160
|
+
try {
|
|
161
|
+
matchesAnchor = sameKey(kmsRootPublicKey, anchors.kmsRootPubkey);
|
|
162
|
+
} catch {
|
|
163
|
+
throw new Error("KMS root trust anchor is malformed");
|
|
164
|
+
}
|
|
165
|
+
if (!matchesAnchor) {
|
|
166
|
+
throw new Error("KMS root public key does not match the trust anchor");
|
|
167
|
+
}
|
|
168
|
+
if ((0, import_viem.keccak256)(kmsRootPublicKey) !== evidence.kmsRootFingerprint.toLowerCase()) {
|
|
169
|
+
throw new Error("KMS root fingerprint does not match the evidence");
|
|
170
|
+
}
|
|
171
|
+
const appId = evidence.appId.toLowerCase();
|
|
172
|
+
if (!anchors.appIds.some((allowedAppId) => allowedAppId.toLowerCase() === appId)) {
|
|
173
|
+
throw new Error("Enclave app ID is not trusted");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function buildMasterSignatureDelivery(evidence, masterSignature, now = Math.floor(Date.now() / 1e3)) {
|
|
177
|
+
(0, import_derive.deriveMasterKey)(masterSignature);
|
|
178
|
+
const signerAddress = await (0, import_derive.recoverServerOwner)(masterSignature);
|
|
179
|
+
if (!(0, import_viem.isAddressEqual)(signerAddress, evidence.ownerAddress)) {
|
|
180
|
+
throw new Error("Master signature signer does not match evidence owner");
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
v: MASTER_SIGNATURE_DELIVERY_VERSION,
|
|
184
|
+
userPsId: evidence.userPsId,
|
|
185
|
+
epoch: evidence.epoch,
|
|
186
|
+
enclaveAddress: evidence.address,
|
|
187
|
+
ownerAddress: evidence.ownerAddress,
|
|
188
|
+
masterSignature,
|
|
189
|
+
issuedAt: now
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async function encryptMasterSignatureDelivery(delivery, publicKey, ecies) {
|
|
193
|
+
assertUncompressedKey(publicKey);
|
|
194
|
+
if (!(0, import_viem.isAddressEqual)((0, import_accounts.publicKeyToAddress)(publicKey), delivery.enclaveAddress)) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
"Public key does not belong to the delivery's enclave address"
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const plaintext = (0, import_viem.toBytes)(JSON.stringify(delivery));
|
|
200
|
+
const encrypted = await ecies.encrypt((0, import_viem.fromHex)(publicKey, "bytes"), plaintext);
|
|
201
|
+
return `0x${(0, import_interface.serializeECIES)(encrypted)}`;
|
|
202
|
+
}
|
|
203
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
204
|
+
0 && (module.exports = {
|
|
205
|
+
ENCLAVE_IDENTITY_EVIDENCE_VERSION,
|
|
206
|
+
ENCLAVE_TRUST_ANCHORS,
|
|
207
|
+
ENCLAVE_WALLET_PURPOSE,
|
|
208
|
+
MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS,
|
|
209
|
+
MASTER_SIGNATURE_DELIVERY_VERSION,
|
|
210
|
+
SEALED_ENVELOPE_VERSION,
|
|
211
|
+
USER_PS_ID_DOMAIN,
|
|
212
|
+
appRootPreimage,
|
|
213
|
+
buildMasterSignatureDelivery,
|
|
214
|
+
encryptMasterSignatureDelivery,
|
|
215
|
+
kmsIssuedPreimage,
|
|
216
|
+
userPsId,
|
|
217
|
+
verifyEnclaveIdentityEvidence
|
|
218
|
+
});
|
|
219
|
+
//# sourceMappingURL=identity.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/protocol/identity.ts"],"sourcesContent":["/**\n * Enclave identity and consent primitives for Personal Servers.\n *\n * Owner/Web -> Gateway -> Agent(CVM): derive wallet and quote\n * Web verifies evidence; Account authenticates and encrypts the EIP-191 master\n * signature to the enclave key; Gateway relays it blind; Agent decrypts/seals.\n *\n * @category Protocol\n */\n\nimport * as secp256k1 from \"@noble/secp256k1\";\nimport {\n concat,\n encodePacked,\n fromHex,\n getAddress,\n isAddressEqual,\n keccak256,\n recoverPublicKey,\n toBytes,\n toHex,\n type Address,\n type Hex,\n} from \"viem\";\nimport { publicKeyToAddress } from \"viem/accounts\";\nimport { serializeECIES, type ECIESProvider } from \"../crypto/ecies/interface\";\nimport { deriveMasterKey, recoverServerOwner } from \"../crypto/keys/derive\";\nimport type { ServerRegistrationMessage } from \"./eip712\";\n\nexport const ENCLAVE_IDENTITY_EVIDENCE_VERSION = 1;\nexport const USER_PS_ID_DOMAIN = \"vana.ps-enclave.v1\";\nexport const ENCLAVE_WALLET_PURPOSE = \"vana.ps-enclave.wallet.v1\";\nexport const MASTER_SIGNATURE_DELIVERY_VERSION = \"vana.ps-enclave.delivery.v1\";\n/** Agent rejects deliveries when `|now - issuedAt|` exceeds this value. */\nexport const MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS = 600;\nexport const SEALED_ENVELOPE_VERSION = 1;\n\nconst VANA_MAINNET_CHAIN_ID = 1480;\nconst MOKSHA_CHAIN_ID = 14800;\nconst KMS_ISSUED_PREFIX = \"dstack-kms-issued\";\nconst PREIMAGE_SEPARATOR = \":\";\nconst UNCOMPRESSED_PUBLIC_KEY_BYTES = 65;\nconst UNCOMPRESSED_PUBLIC_KEY_PREFIX = \"04\";\nconst EMPTY_HEX = \"0x\";\n\n/** `keccak256(encodePacked([string,uint256,address], [USER_PS_ID_DOMAIN,chainId,owner]))`. */\nexport type UserPsId = Hex;\n\n/** Attested identity evidence returned by the enclave agent. */\nexport interface EnclaveIdentityEvidence {\n v: typeof ENCLAVE_IDENTITY_EVIDENCE_VERSION;\n userPsId: UserPsId;\n chainId: number;\n ownerAddress: Address;\n /** Path suffix `users/{id}/wallet/ethereum/secp256k1/v{epoch}`. */\n epoch: number;\n address: Address;\n /** 65-byte uncompressed `0x04..`; `publicKeyToAddress(publicKey) == address`. */\n publicKey: Hex;\n /** 20-byte dstack `app_id`. */\n appId: Hex;\n /** 32-byte compose hash. */\n composeHash: Hex;\n /** Omitted when the OS does not expose its image hash. */\n osImageHash?: Hex;\n /** Must equal `ENCLAVE_WALLET_PURPOSE`. */\n purpose: string;\n /** `[appRoot over link 0, kmsRoot over link 1]`; see `appRootPreimage` and `kmsIssuedPreimage`. */\n signatureChain: [Hex, Hex];\n /** Raw TDX quote with report_data `keccak256(userPsId || address)`; not parsed. */\n quote: Hex;\n eventLog?: string;\n /** `keccak256` of the uncompressed KMS root public key. */\n kmsRootFingerprint: Hex;\n}\n\n/** Identity values the caller expects the evidence to bind. */\nexport interface ExpectedIdentity {\n ownerAddress: Address;\n chainId: number;\n epoch: number;\n}\n\n/** Request for an owner-scoped enclave identity. */\nexport interface IdentityRequest {\n ownerAddress: Address;\n chainId: number;\n}\n\n/** Lifecycle state of an enclave identity. */\nexport type IdentityState = \"prepared\" | \"registered\" | \"sealed\" | \"retired\";\n\n/** Gateway response containing enclave identity state and registration data. */\nexport interface IdentityResponse {\n identity: EnclaveIdentityEvidence;\n state: IdentityState;\n created: boolean;\n /** Exact `ServerRegistration.serverUrl` the owner signs. */\n serverUrl: string;\n serverId?: Hex;\n serverStatus?:\n | \"pending\"\n | \"submitting\"\n | \"confirmed\"\n | \"finalized\"\n | \"failed\";\n sealed: boolean;\n}\n\n/** Versioned server-registration request for an enclave identity. */\nexport type IdentityRegistrationRequest =\n | { version: \"v2\"; message: ServerRegistrationMessage }\n | {\n version: \"v3\";\n message: ServerRegistrationMessage & { nonce: string; deadline: string };\n };\n\n/** Accepted identity-registration response. */\nexport interface IdentityRegistrationResponse {\n serverId: Hex;\n state: \"registered\";\n serverStatus: \"pending\";\n}\n\n/** Inner plaintext of the ECIES box. */\nexport interface MasterSignatureDelivery {\n v: typeof MASTER_SIGNATURE_DELIVERY_VERSION;\n userPsId: UserPsId;\n epoch: number;\n enclaveAddress: Address;\n ownerAddress: Address;\n /** 65-byte EIP-191 signature over `MASTER_KEY_MESSAGE`. */\n masterSignature: Hex;\n /** Unix seconds; see `MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS`. */\n issuedAt: number;\n}\n\n/** Enclave-bound ciphertext submitted for sealing. */\nexport interface SealedSecretSubmission {\n userPsId: UserPsId;\n epoch: number;\n enclaveAddress: Address;\n /** `iv(16) || ephemPub(65) || ct || mac(32)` to `evidence.publicKey`. */\n ciphertext: Hex;\n}\n\n/** Confirmation that the enclave sealed a submitted secret. */\nexport interface SealedSecretResponse {\n sealed: true;\n /** `sha256(ciphertext)`. */\n secretHash: Hex;\n sealedAt: string;\n}\n\n/** AES-GCM fields encoded as base64 strings. */\nexport interface AesGcmBox {\n /** Base64 initialization vector. */\n iv: string;\n /** Base64 ciphertext. */\n ciphertext: string;\n /** Base64 authentication tag. */\n tag: string;\n}\n\n/** Persisted enclave envelope; the Gateway treats it as opaque text. */\nexport interface SealedEnvelope extends AesGcmBox {\n v: typeof SEALED_ENVELOPE_VERSION;\n wrappedContentKey: AesGcmBox;\n}\n\n/** Fleet-pinned KMS root and allowed dstack application IDs. */\nexport interface EnclaveTrustAnchors {\n kmsRootPubkey: Hex;\n appIds: readonly Hex[];\n}\n\nfunction emptyAnchor(): Readonly<EnclaveTrustAnchors> {\n return Object.freeze({\n kmsRootPubkey: EMPTY_HEX,\n appIds: Object.freeze([] as Hex[]),\n });\n}\n\n/** Fleet-provisioned trust anchors keyed by Vana chain ID. */\nexport const ENCLAVE_TRUST_ANCHORS: Readonly<\n Record<number, Readonly<EnclaveTrustAnchors>>\n> = Object.freeze({\n // filled at fleet provisioning; verify fails closed while empty\n [VANA_MAINNET_CHAIN_ID]: emptyAnchor(),\n // filled at fleet provisioning; verify fails closed while empty\n [MOKSHA_CHAIN_ID]: emptyAnchor(),\n});\n\n/**\n * Derives the deterministic Personal Server ID used by enclave paths.\n *\n * @param chainId - Vana chain ID.\n * @param ownerAddress - Checksummed owner address.\n * @returns The path-compatible keccak256 digest.\n */\nexport function userPsId(chainId: number, ownerAddress: Address): UserPsId {\n const packed = encodePacked(\n [\"string\", \"uint256\", \"address\"],\n [USER_PS_ID_DOMAIN, BigInt(chainId), getAddress(ownerAddress)],\n );\n\n return keccak256(packed);\n}\n\nfunction compressPublicKey(publicKey: Hex): Uint8Array {\n return secp256k1.ProjectivePoint.fromHex(\n fromHex(publicKey, \"bytes\"),\n ).toRawBytes(true);\n}\n\nfunction sameKey(a: Hex, b: Hex): boolean {\n return toHex(compressPublicKey(a)) === toHex(compressPublicKey(b));\n}\n\nfunction assertUncompressedKey(publicKey: Hex): void {\n const hex = publicKey.slice(2);\n if (\n hex.length !== UNCOMPRESSED_PUBLIC_KEY_BYTES * 2 ||\n !hex.startsWith(UNCOMPRESSED_PUBLIC_KEY_PREFIX) ||\n !/^[0-9a-fA-F]+$/.test(hex)\n ) {\n throw new Error(\"Public key must be a 65-byte uncompressed secp256k1 key\");\n }\n}\n\n/**\n * Builds keccak256(utf8(purpose || \":\" || lowercase hex(compressed pubkey))).\n *\n * Matches `dstack/guest-agent/src/rpc_service.rs:612-628`.\n */\nexport function appRootPreimage(purpose: string, publicKey: Hex): Hex {\n const keyHex = toHex(compressPublicKey(publicKey)).slice(2);\n return keccak256(toBytes(`${purpose}${PREIMAGE_SEPARATOR}${keyHex}`));\n}\n\n/**\n * Hashes the KMS prefix, raw app ID bytes, and compressed app-root public key.\n *\n * Matches `dstack/kms/src/crypto.rs:23-40`; there is no separator between the\n * raw 20-byte app ID and the compressed key.\n */\nexport function kmsIssuedPreimage(appId: Hex, appRootPublicKey: Hex): Hex {\n const prefix = concat([\n toBytes(`${KMS_ISSUED_PREFIX}${PREIMAGE_SEPARATOR}`),\n fromHex(appId, \"bytes\"),\n ]);\n const compressed = compressPublicKey(appRootPublicKey);\n\n return keccak256(concat([prefix, compressed]));\n}\n\nasync function recoverChainKey(\n hash: Hex,\n signature: Hex,\n link: number,\n): Promise<Hex> {\n try {\n return (await recoverPublicKey({ hash, signature })).toLowerCase() as Hex;\n } catch {\n throw new Error(`Invalid enclave signature chain link ${link}`);\n }\n}\n\n/**\n * Verifies the enclave wallet binding against fleet-provisioned trust anchors.\n *\n * @param evidence - Identity evidence returned by the enclave agent.\n * @param anchors - Pinned KMS root and allowed dstack app IDs.\n * @param expected - Owner, chain, and epoch requested by the caller.\n *\n * @remarks\n * The dstack chain signs only `purpose || publicKey` and `appId || appRootPub`;\n * owner, chain and epoch are bound to the key by the TDX quote report_data\n * (`keccak256(userPsId || address)`), which v1 stores but does not verify\n * (DCAP is step 4). Until then the agent's derivation check is the backstop.\n * @throws When any identity binding or trust check fails.\n */\nexport async function verifyEnclaveIdentityEvidence(\n evidence: EnclaveIdentityEvidence,\n anchors: EnclaveTrustAnchors,\n expected: ExpectedIdentity,\n): Promise<void> {\n if (evidence.v !== ENCLAVE_IDENTITY_EVIDENCE_VERSION) {\n throw new Error(\"Unsupported enclave identity evidence version\");\n }\n\n if (!Number.isInteger(evidence.epoch) || evidence.epoch < 1) {\n throw new Error(\"Invalid enclave identity epoch\");\n }\n\n if (evidence.chainId !== expected.chainId) {\n throw new Error(\n \"Enclave identity chain ID does not match expected chain ID\",\n );\n }\n\n if (!isAddressEqual(evidence.ownerAddress, expected.ownerAddress)) {\n throw new Error(\"Enclave identity owner does not match expected owner\");\n }\n\n if (evidence.epoch !== expected.epoch) {\n throw new Error(\"Enclave identity epoch does not match expected epoch\");\n }\n\n const expectedUserPsId = userPsId(expected.chainId, expected.ownerAddress);\n if (evidence.userPsId.toLowerCase() !== expectedUserPsId.toLowerCase()) {\n throw new Error(\"Enclave userPsId does not match expected identity\");\n }\n\n if (evidence.purpose !== ENCLAVE_WALLET_PURPOSE) {\n throw new Error(\"Unexpected enclave wallet purpose\");\n }\n\n if (anchors.kmsRootPubkey === EMPTY_HEX) {\n throw new Error(\"KMS root trust anchor is not provisioned\");\n }\n\n assertUncompressedKey(evidence.publicKey);\n const derivedAddress = publicKeyToAddress(evidence.publicKey);\n\n if (getAddress(derivedAddress) !== getAddress(evidence.address)) {\n throw new Error(\"Enclave public key does not match its address\");\n }\n\n const appRootPublicKey = await recoverChainKey(\n appRootPreimage(evidence.purpose, evidence.publicKey),\n evidence.signatureChain[0],\n 0,\n );\n const kmsRootPublicKey = await recoverChainKey(\n kmsIssuedPreimage(evidence.appId, appRootPublicKey),\n evidence.signatureChain[1],\n 1,\n );\n\n let matchesAnchor: boolean;\n try {\n matchesAnchor = sameKey(kmsRootPublicKey, anchors.kmsRootPubkey);\n } catch {\n throw new Error(\"KMS root trust anchor is malformed\");\n }\n\n if (!matchesAnchor) {\n throw new Error(\"KMS root public key does not match the trust anchor\");\n }\n\n if (\n keccak256(kmsRootPublicKey) !== evidence.kmsRootFingerprint.toLowerCase()\n ) {\n throw new Error(\"KMS root fingerprint does not match the evidence\");\n }\n\n const appId = evidence.appId.toLowerCase();\n if (\n !anchors.appIds.some((allowedAppId) => allowedAppId.toLowerCase() === appId)\n ) {\n throw new Error(\"Enclave app ID is not trusted\");\n }\n}\n\n/**\n * Builds the authenticated master-signature plaintext delivered to an enclave.\n *\n * @param evidence - Verified enclave identity evidence.\n * @param masterSignature - EIP-191 signature over the master-key message.\n * @param now - Issuance time in Unix seconds.\n * @returns The authenticated plaintext delivery object.\n * @throws When the signature is malformed or does not belong to the owner.\n */\nexport async function buildMasterSignatureDelivery(\n evidence: EnclaveIdentityEvidence,\n masterSignature: Hex,\n now = Math.floor(Date.now() / 1000),\n): Promise<MasterSignatureDelivery> {\n // Validate signature length and hex encoding before recovery.\n deriveMasterKey(masterSignature);\n const signerAddress = await recoverServerOwner(masterSignature);\n\n if (!isAddressEqual(signerAddress, evidence.ownerAddress)) {\n throw new Error(\"Master signature signer does not match evidence owner\");\n }\n\n return {\n v: MASTER_SIGNATURE_DELIVERY_VERSION,\n userPsId: evidence.userPsId,\n epoch: evidence.epoch,\n enclaveAddress: evidence.address,\n ownerAddress: evidence.ownerAddress,\n masterSignature,\n issuedAt: now,\n };\n}\n\n/**\n * Encrypts a master-signature delivery in the SDK's ECIES wire format.\n *\n * @param delivery - Plaintext delivery object.\n * @param publicKey - Enclave 65-byte uncompressed public key.\n * @param ecies - Platform-specific ECIES provider.\n * @returns The 0x-prefixed serialized ciphertext.\n * @throws When the public key is malformed or belongs to another enclave.\n */\nexport async function encryptMasterSignatureDelivery(\n delivery: MasterSignatureDelivery,\n publicKey: Hex,\n ecies: ECIESProvider,\n): Promise<Hex> {\n assertUncompressedKey(publicKey);\n\n if (!isAddressEqual(publicKeyToAddress(publicKey), delivery.enclaveAddress)) {\n throw new Error(\n \"Public key does not belong to the delivery's enclave address\",\n );\n }\n\n const plaintext = toBytes(JSON.stringify(delivery));\n const encrypted = await ecies.encrypt(fromHex(publicKey, \"bytes\"), plaintext);\n\n return `0x${serializeECIES(encrypted)}`;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enclave identity and consent primitives for Personal Servers.
|
|
3
|
+
*
|
|
4
|
+
* Owner/Web -> Gateway -> Agent(CVM): derive wallet and quote
|
|
5
|
+
* Web verifies evidence; Account authenticates and encrypts the EIP-191 master
|
|
6
|
+
* signature to the enclave key; Gateway relays it blind; Agent decrypts/seals.
|
|
7
|
+
*
|
|
8
|
+
* @category Protocol
|
|
9
|
+
*/
|
|
10
|
+
import { type Address, type Hex } from "viem";
|
|
11
|
+
import { type ECIESProvider } from "../crypto/ecies/interface.js";
|
|
12
|
+
import type { ServerRegistrationMessage } from "./eip712.js";
|
|
13
|
+
export declare const ENCLAVE_IDENTITY_EVIDENCE_VERSION = 1;
|
|
14
|
+
export declare const USER_PS_ID_DOMAIN = "vana.ps-enclave.v1";
|
|
15
|
+
export declare const ENCLAVE_WALLET_PURPOSE = "vana.ps-enclave.wallet.v1";
|
|
16
|
+
export declare const MASTER_SIGNATURE_DELIVERY_VERSION = "vana.ps-enclave.delivery.v1";
|
|
17
|
+
/** Agent rejects deliveries when `|now - issuedAt|` exceeds this value. */
|
|
18
|
+
export declare const MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS = 600;
|
|
19
|
+
export declare const SEALED_ENVELOPE_VERSION = 1;
|
|
20
|
+
/** `keccak256(encodePacked([string,uint256,address], [USER_PS_ID_DOMAIN,chainId,owner]))`. */
|
|
21
|
+
export type UserPsId = Hex;
|
|
22
|
+
/** Attested identity evidence returned by the enclave agent. */
|
|
23
|
+
export interface EnclaveIdentityEvidence {
|
|
24
|
+
v: typeof ENCLAVE_IDENTITY_EVIDENCE_VERSION;
|
|
25
|
+
userPsId: UserPsId;
|
|
26
|
+
chainId: number;
|
|
27
|
+
ownerAddress: Address;
|
|
28
|
+
/** Path suffix `users/{id}/wallet/ethereum/secp256k1/v{epoch}`. */
|
|
29
|
+
epoch: number;
|
|
30
|
+
address: Address;
|
|
31
|
+
/** 65-byte uncompressed `0x04..`; `publicKeyToAddress(publicKey) == address`. */
|
|
32
|
+
publicKey: Hex;
|
|
33
|
+
/** 20-byte dstack `app_id`. */
|
|
34
|
+
appId: Hex;
|
|
35
|
+
/** 32-byte compose hash. */
|
|
36
|
+
composeHash: Hex;
|
|
37
|
+
/** Omitted when the OS does not expose its image hash. */
|
|
38
|
+
osImageHash?: Hex;
|
|
39
|
+
/** Must equal `ENCLAVE_WALLET_PURPOSE`. */
|
|
40
|
+
purpose: string;
|
|
41
|
+
/** `[appRoot over link 0, kmsRoot over link 1]`; see `appRootPreimage` and `kmsIssuedPreimage`. */
|
|
42
|
+
signatureChain: [Hex, Hex];
|
|
43
|
+
/** Raw TDX quote with report_data `keccak256(userPsId || address)`; not parsed. */
|
|
44
|
+
quote: Hex;
|
|
45
|
+
eventLog?: string;
|
|
46
|
+
/** `keccak256` of the uncompressed KMS root public key. */
|
|
47
|
+
kmsRootFingerprint: Hex;
|
|
48
|
+
}
|
|
49
|
+
/** Identity values the caller expects the evidence to bind. */
|
|
50
|
+
export interface ExpectedIdentity {
|
|
51
|
+
ownerAddress: Address;
|
|
52
|
+
chainId: number;
|
|
53
|
+
epoch: number;
|
|
54
|
+
}
|
|
55
|
+
/** Request for an owner-scoped enclave identity. */
|
|
56
|
+
export interface IdentityRequest {
|
|
57
|
+
ownerAddress: Address;
|
|
58
|
+
chainId: number;
|
|
59
|
+
}
|
|
60
|
+
/** Lifecycle state of an enclave identity. */
|
|
61
|
+
export type IdentityState = "prepared" | "registered" | "sealed" | "retired";
|
|
62
|
+
/** Gateway response containing enclave identity state and registration data. */
|
|
63
|
+
export interface IdentityResponse {
|
|
64
|
+
identity: EnclaveIdentityEvidence;
|
|
65
|
+
state: IdentityState;
|
|
66
|
+
created: boolean;
|
|
67
|
+
/** Exact `ServerRegistration.serverUrl` the owner signs. */
|
|
68
|
+
serverUrl: string;
|
|
69
|
+
serverId?: Hex;
|
|
70
|
+
serverStatus?: "pending" | "submitting" | "confirmed" | "finalized" | "failed";
|
|
71
|
+
sealed: boolean;
|
|
72
|
+
}
|
|
73
|
+
/** Versioned server-registration request for an enclave identity. */
|
|
74
|
+
export type IdentityRegistrationRequest = {
|
|
75
|
+
version: "v2";
|
|
76
|
+
message: ServerRegistrationMessage;
|
|
77
|
+
} | {
|
|
78
|
+
version: "v3";
|
|
79
|
+
message: ServerRegistrationMessage & {
|
|
80
|
+
nonce: string;
|
|
81
|
+
deadline: string;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
/** Accepted identity-registration response. */
|
|
85
|
+
export interface IdentityRegistrationResponse {
|
|
86
|
+
serverId: Hex;
|
|
87
|
+
state: "registered";
|
|
88
|
+
serverStatus: "pending";
|
|
89
|
+
}
|
|
90
|
+
/** Inner plaintext of the ECIES box. */
|
|
91
|
+
export interface MasterSignatureDelivery {
|
|
92
|
+
v: typeof MASTER_SIGNATURE_DELIVERY_VERSION;
|
|
93
|
+
userPsId: UserPsId;
|
|
94
|
+
epoch: number;
|
|
95
|
+
enclaveAddress: Address;
|
|
96
|
+
ownerAddress: Address;
|
|
97
|
+
/** 65-byte EIP-191 signature over `MASTER_KEY_MESSAGE`. */
|
|
98
|
+
masterSignature: Hex;
|
|
99
|
+
/** Unix seconds; see `MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS`. */
|
|
100
|
+
issuedAt: number;
|
|
101
|
+
}
|
|
102
|
+
/** Enclave-bound ciphertext submitted for sealing. */
|
|
103
|
+
export interface SealedSecretSubmission {
|
|
104
|
+
userPsId: UserPsId;
|
|
105
|
+
epoch: number;
|
|
106
|
+
enclaveAddress: Address;
|
|
107
|
+
/** `iv(16) || ephemPub(65) || ct || mac(32)` to `evidence.publicKey`. */
|
|
108
|
+
ciphertext: Hex;
|
|
109
|
+
}
|
|
110
|
+
/** Confirmation that the enclave sealed a submitted secret. */
|
|
111
|
+
export interface SealedSecretResponse {
|
|
112
|
+
sealed: true;
|
|
113
|
+
/** `sha256(ciphertext)`. */
|
|
114
|
+
secretHash: Hex;
|
|
115
|
+
sealedAt: string;
|
|
116
|
+
}
|
|
117
|
+
/** AES-GCM fields encoded as base64 strings. */
|
|
118
|
+
export interface AesGcmBox {
|
|
119
|
+
/** Base64 initialization vector. */
|
|
120
|
+
iv: string;
|
|
121
|
+
/** Base64 ciphertext. */
|
|
122
|
+
ciphertext: string;
|
|
123
|
+
/** Base64 authentication tag. */
|
|
124
|
+
tag: string;
|
|
125
|
+
}
|
|
126
|
+
/** Persisted enclave envelope; the Gateway treats it as opaque text. */
|
|
127
|
+
export interface SealedEnvelope extends AesGcmBox {
|
|
128
|
+
v: typeof SEALED_ENVELOPE_VERSION;
|
|
129
|
+
wrappedContentKey: AesGcmBox;
|
|
130
|
+
}
|
|
131
|
+
/** Fleet-pinned KMS root and allowed dstack application IDs. */
|
|
132
|
+
export interface EnclaveTrustAnchors {
|
|
133
|
+
kmsRootPubkey: Hex;
|
|
134
|
+
appIds: readonly Hex[];
|
|
135
|
+
}
|
|
136
|
+
/** Fleet-provisioned trust anchors keyed by Vana chain ID. */
|
|
137
|
+
export declare const ENCLAVE_TRUST_ANCHORS: Readonly<Record<number, Readonly<EnclaveTrustAnchors>>>;
|
|
138
|
+
/**
|
|
139
|
+
* Derives the deterministic Personal Server ID used by enclave paths.
|
|
140
|
+
*
|
|
141
|
+
* @param chainId - Vana chain ID.
|
|
142
|
+
* @param ownerAddress - Checksummed owner address.
|
|
143
|
+
* @returns The path-compatible keccak256 digest.
|
|
144
|
+
*/
|
|
145
|
+
export declare function userPsId(chainId: number, ownerAddress: Address): UserPsId;
|
|
146
|
+
/**
|
|
147
|
+
* Builds keccak256(utf8(purpose || ":" || lowercase hex(compressed pubkey))).
|
|
148
|
+
*
|
|
149
|
+
* Matches `dstack/guest-agent/src/rpc_service.rs:612-628`.
|
|
150
|
+
*/
|
|
151
|
+
export declare function appRootPreimage(purpose: string, publicKey: Hex): Hex;
|
|
152
|
+
/**
|
|
153
|
+
* Hashes the KMS prefix, raw app ID bytes, and compressed app-root public key.
|
|
154
|
+
*
|
|
155
|
+
* Matches `dstack/kms/src/crypto.rs:23-40`; there is no separator between the
|
|
156
|
+
* raw 20-byte app ID and the compressed key.
|
|
157
|
+
*/
|
|
158
|
+
export declare function kmsIssuedPreimage(appId: Hex, appRootPublicKey: Hex): Hex;
|
|
159
|
+
/**
|
|
160
|
+
* Verifies the enclave wallet binding against fleet-provisioned trust anchors.
|
|
161
|
+
*
|
|
162
|
+
* @param evidence - Identity evidence returned by the enclave agent.
|
|
163
|
+
* @param anchors - Pinned KMS root and allowed dstack app IDs.
|
|
164
|
+
* @param expected - Owner, chain, and epoch requested by the caller.
|
|
165
|
+
*
|
|
166
|
+
* @remarks
|
|
167
|
+
* The dstack chain signs only `purpose || publicKey` and `appId || appRootPub`;
|
|
168
|
+
* owner, chain and epoch are bound to the key by the TDX quote report_data
|
|
169
|
+
* (`keccak256(userPsId || address)`), which v1 stores but does not verify
|
|
170
|
+
* (DCAP is step 4). Until then the agent's derivation check is the backstop.
|
|
171
|
+
* @throws When any identity binding or trust check fails.
|
|
172
|
+
*/
|
|
173
|
+
export declare function verifyEnclaveIdentityEvidence(evidence: EnclaveIdentityEvidence, anchors: EnclaveTrustAnchors, expected: ExpectedIdentity): Promise<void>;
|
|
174
|
+
/**
|
|
175
|
+
* Builds the authenticated master-signature plaintext delivered to an enclave.
|
|
176
|
+
*
|
|
177
|
+
* @param evidence - Verified enclave identity evidence.
|
|
178
|
+
* @param masterSignature - EIP-191 signature over the master-key message.
|
|
179
|
+
* @param now - Issuance time in Unix seconds.
|
|
180
|
+
* @returns The authenticated plaintext delivery object.
|
|
181
|
+
* @throws When the signature is malformed or does not belong to the owner.
|
|
182
|
+
*/
|
|
183
|
+
export declare function buildMasterSignatureDelivery(evidence: EnclaveIdentityEvidence, masterSignature: Hex, now?: number): Promise<MasterSignatureDelivery>;
|
|
184
|
+
/**
|
|
185
|
+
* Encrypts a master-signature delivery in the SDK's ECIES wire format.
|
|
186
|
+
*
|
|
187
|
+
* @param delivery - Plaintext delivery object.
|
|
188
|
+
* @param publicKey - Enclave 65-byte uncompressed public key.
|
|
189
|
+
* @param ecies - Platform-specific ECIES provider.
|
|
190
|
+
* @returns The 0x-prefixed serialized ciphertext.
|
|
191
|
+
* @throws When the public key is malformed or belongs to another enclave.
|
|
192
|
+
*/
|
|
193
|
+
export declare function encryptMasterSignatureDelivery(delivery: MasterSignatureDelivery, publicKey: Hex, ecies: ECIESProvider): Promise<Hex>;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import * as secp256k1 from "@noble/secp256k1";
|
|
2
|
+
import {
|
|
3
|
+
concat,
|
|
4
|
+
encodePacked,
|
|
5
|
+
fromHex,
|
|
6
|
+
getAddress,
|
|
7
|
+
isAddressEqual,
|
|
8
|
+
keccak256,
|
|
9
|
+
recoverPublicKey,
|
|
10
|
+
toBytes,
|
|
11
|
+
toHex
|
|
12
|
+
} from "viem";
|
|
13
|
+
import { publicKeyToAddress } from "viem/accounts";
|
|
14
|
+
import { serializeECIES } from "../crypto/ecies/interface.js";
|
|
15
|
+
import { deriveMasterKey, recoverServerOwner } from "../crypto/keys/derive.js";
|
|
16
|
+
const ENCLAVE_IDENTITY_EVIDENCE_VERSION = 1;
|
|
17
|
+
const USER_PS_ID_DOMAIN = "vana.ps-enclave.v1";
|
|
18
|
+
const ENCLAVE_WALLET_PURPOSE = "vana.ps-enclave.wallet.v1";
|
|
19
|
+
const MASTER_SIGNATURE_DELIVERY_VERSION = "vana.ps-enclave.delivery.v1";
|
|
20
|
+
const MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS = 600;
|
|
21
|
+
const SEALED_ENVELOPE_VERSION = 1;
|
|
22
|
+
const VANA_MAINNET_CHAIN_ID = 1480;
|
|
23
|
+
const MOKSHA_CHAIN_ID = 14800;
|
|
24
|
+
const KMS_ISSUED_PREFIX = "dstack-kms-issued";
|
|
25
|
+
const PREIMAGE_SEPARATOR = ":";
|
|
26
|
+
const UNCOMPRESSED_PUBLIC_KEY_BYTES = 65;
|
|
27
|
+
const UNCOMPRESSED_PUBLIC_KEY_PREFIX = "04";
|
|
28
|
+
const EMPTY_HEX = "0x";
|
|
29
|
+
function emptyAnchor() {
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
kmsRootPubkey: EMPTY_HEX,
|
|
32
|
+
appIds: Object.freeze([])
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const ENCLAVE_TRUST_ANCHORS = Object.freeze({
|
|
36
|
+
// filled at fleet provisioning; verify fails closed while empty
|
|
37
|
+
[VANA_MAINNET_CHAIN_ID]: emptyAnchor(),
|
|
38
|
+
// filled at fleet provisioning; verify fails closed while empty
|
|
39
|
+
[MOKSHA_CHAIN_ID]: emptyAnchor()
|
|
40
|
+
});
|
|
41
|
+
function userPsId(chainId, ownerAddress) {
|
|
42
|
+
const packed = encodePacked(
|
|
43
|
+
["string", "uint256", "address"],
|
|
44
|
+
[USER_PS_ID_DOMAIN, BigInt(chainId), getAddress(ownerAddress)]
|
|
45
|
+
);
|
|
46
|
+
return keccak256(packed);
|
|
47
|
+
}
|
|
48
|
+
function compressPublicKey(publicKey) {
|
|
49
|
+
return secp256k1.ProjectivePoint.fromHex(
|
|
50
|
+
fromHex(publicKey, "bytes")
|
|
51
|
+
).toRawBytes(true);
|
|
52
|
+
}
|
|
53
|
+
function sameKey(a, b) {
|
|
54
|
+
return toHex(compressPublicKey(a)) === toHex(compressPublicKey(b));
|
|
55
|
+
}
|
|
56
|
+
function assertUncompressedKey(publicKey) {
|
|
57
|
+
const hex = publicKey.slice(2);
|
|
58
|
+
if (hex.length !== UNCOMPRESSED_PUBLIC_KEY_BYTES * 2 || !hex.startsWith(UNCOMPRESSED_PUBLIC_KEY_PREFIX) || !/^[0-9a-fA-F]+$/.test(hex)) {
|
|
59
|
+
throw new Error("Public key must be a 65-byte uncompressed secp256k1 key");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function appRootPreimage(purpose, publicKey) {
|
|
63
|
+
const keyHex = toHex(compressPublicKey(publicKey)).slice(2);
|
|
64
|
+
return keccak256(toBytes(`${purpose}${PREIMAGE_SEPARATOR}${keyHex}`));
|
|
65
|
+
}
|
|
66
|
+
function kmsIssuedPreimage(appId, appRootPublicKey) {
|
|
67
|
+
const prefix = concat([
|
|
68
|
+
toBytes(`${KMS_ISSUED_PREFIX}${PREIMAGE_SEPARATOR}`),
|
|
69
|
+
fromHex(appId, "bytes")
|
|
70
|
+
]);
|
|
71
|
+
const compressed = compressPublicKey(appRootPublicKey);
|
|
72
|
+
return keccak256(concat([prefix, compressed]));
|
|
73
|
+
}
|
|
74
|
+
async function recoverChainKey(hash, signature, link) {
|
|
75
|
+
try {
|
|
76
|
+
return (await recoverPublicKey({ hash, signature })).toLowerCase();
|
|
77
|
+
} catch {
|
|
78
|
+
throw new Error(`Invalid enclave signature chain link ${link}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function verifyEnclaveIdentityEvidence(evidence, anchors, expected) {
|
|
82
|
+
if (evidence.v !== ENCLAVE_IDENTITY_EVIDENCE_VERSION) {
|
|
83
|
+
throw new Error("Unsupported enclave identity evidence version");
|
|
84
|
+
}
|
|
85
|
+
if (!Number.isInteger(evidence.epoch) || evidence.epoch < 1) {
|
|
86
|
+
throw new Error("Invalid enclave identity epoch");
|
|
87
|
+
}
|
|
88
|
+
if (evidence.chainId !== expected.chainId) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"Enclave identity chain ID does not match expected chain ID"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (!isAddressEqual(evidence.ownerAddress, expected.ownerAddress)) {
|
|
94
|
+
throw new Error("Enclave identity owner does not match expected owner");
|
|
95
|
+
}
|
|
96
|
+
if (evidence.epoch !== expected.epoch) {
|
|
97
|
+
throw new Error("Enclave identity epoch does not match expected epoch");
|
|
98
|
+
}
|
|
99
|
+
const expectedUserPsId = userPsId(expected.chainId, expected.ownerAddress);
|
|
100
|
+
if (evidence.userPsId.toLowerCase() !== expectedUserPsId.toLowerCase()) {
|
|
101
|
+
throw new Error("Enclave userPsId does not match expected identity");
|
|
102
|
+
}
|
|
103
|
+
if (evidence.purpose !== ENCLAVE_WALLET_PURPOSE) {
|
|
104
|
+
throw new Error("Unexpected enclave wallet purpose");
|
|
105
|
+
}
|
|
106
|
+
if (anchors.kmsRootPubkey === EMPTY_HEX) {
|
|
107
|
+
throw new Error("KMS root trust anchor is not provisioned");
|
|
108
|
+
}
|
|
109
|
+
assertUncompressedKey(evidence.publicKey);
|
|
110
|
+
const derivedAddress = publicKeyToAddress(evidence.publicKey);
|
|
111
|
+
if (getAddress(derivedAddress) !== getAddress(evidence.address)) {
|
|
112
|
+
throw new Error("Enclave public key does not match its address");
|
|
113
|
+
}
|
|
114
|
+
const appRootPublicKey = await recoverChainKey(
|
|
115
|
+
appRootPreimage(evidence.purpose, evidence.publicKey),
|
|
116
|
+
evidence.signatureChain[0],
|
|
117
|
+
0
|
|
118
|
+
);
|
|
119
|
+
const kmsRootPublicKey = await recoverChainKey(
|
|
120
|
+
kmsIssuedPreimage(evidence.appId, appRootPublicKey),
|
|
121
|
+
evidence.signatureChain[1],
|
|
122
|
+
1
|
|
123
|
+
);
|
|
124
|
+
let matchesAnchor;
|
|
125
|
+
try {
|
|
126
|
+
matchesAnchor = sameKey(kmsRootPublicKey, anchors.kmsRootPubkey);
|
|
127
|
+
} catch {
|
|
128
|
+
throw new Error("KMS root trust anchor is malformed");
|
|
129
|
+
}
|
|
130
|
+
if (!matchesAnchor) {
|
|
131
|
+
throw new Error("KMS root public key does not match the trust anchor");
|
|
132
|
+
}
|
|
133
|
+
if (keccak256(kmsRootPublicKey) !== evidence.kmsRootFingerprint.toLowerCase()) {
|
|
134
|
+
throw new Error("KMS root fingerprint does not match the evidence");
|
|
135
|
+
}
|
|
136
|
+
const appId = evidence.appId.toLowerCase();
|
|
137
|
+
if (!anchors.appIds.some((allowedAppId) => allowedAppId.toLowerCase() === appId)) {
|
|
138
|
+
throw new Error("Enclave app ID is not trusted");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function buildMasterSignatureDelivery(evidence, masterSignature, now = Math.floor(Date.now() / 1e3)) {
|
|
142
|
+
deriveMasterKey(masterSignature);
|
|
143
|
+
const signerAddress = await recoverServerOwner(masterSignature);
|
|
144
|
+
if (!isAddressEqual(signerAddress, evidence.ownerAddress)) {
|
|
145
|
+
throw new Error("Master signature signer does not match evidence owner");
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
v: MASTER_SIGNATURE_DELIVERY_VERSION,
|
|
149
|
+
userPsId: evidence.userPsId,
|
|
150
|
+
epoch: evidence.epoch,
|
|
151
|
+
enclaveAddress: evidence.address,
|
|
152
|
+
ownerAddress: evidence.ownerAddress,
|
|
153
|
+
masterSignature,
|
|
154
|
+
issuedAt: now
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async function encryptMasterSignatureDelivery(delivery, publicKey, ecies) {
|
|
158
|
+
assertUncompressedKey(publicKey);
|
|
159
|
+
if (!isAddressEqual(publicKeyToAddress(publicKey), delivery.enclaveAddress)) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
"Public key does not belong to the delivery's enclave address"
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
const plaintext = toBytes(JSON.stringify(delivery));
|
|
165
|
+
const encrypted = await ecies.encrypt(fromHex(publicKey, "bytes"), plaintext);
|
|
166
|
+
return `0x${serializeECIES(encrypted)}`;
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
ENCLAVE_IDENTITY_EVIDENCE_VERSION,
|
|
170
|
+
ENCLAVE_TRUST_ANCHORS,
|
|
171
|
+
ENCLAVE_WALLET_PURPOSE,
|
|
172
|
+
MASTER_SIGNATURE_DELIVERY_MAX_AGE_SECONDS,
|
|
173
|
+
MASTER_SIGNATURE_DELIVERY_VERSION,
|
|
174
|
+
SEALED_ENVELOPE_VERSION,
|
|
175
|
+
USER_PS_ID_DOMAIN,
|
|
176
|
+
appRootPreimage,
|
|
177
|
+
buildMasterSignatureDelivery,
|
|
178
|
+
encryptMasterSignatureDelivery,
|
|
179
|
+
kmsIssuedPreimage,
|
|
180
|
+
userPsId,
|
|
181
|
+
verifyEnclaveIdentityEvidence
|
|
182
|
+
};
|
|
183
|
+
//# sourceMappingURL=identity.js.map
|