@tdxvolt/volt-client-grpc 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/README.md +5 -0
- package/lib/index.cjs +1883 -0
- package/package.json +50 -0
- package/protobuf/README.md +4 -0
- package/protobuf/tdx/volt_api/data/v1/sqlite.proto +43 -0
- package/protobuf/tdx/volt_api/data/v1/sqlite_database_api.proto +105 -0
- package/protobuf/tdx/volt_api/data/v1/sqlite_server_api.proto +42 -0
- package/protobuf/tdx/volt_api/ssi/v1/ssi_api.proto +37 -0
- package/protobuf/tdx/volt_api/sync/v1/sync.proto +14 -0
- package/protobuf/tdx/volt_api/tunnel/v1/proxy_api.proto +9 -0
- package/protobuf/tdx/volt_api/tunnel/v1/tunnel_api.proto +77 -0
- package/protobuf/tdx/volt_api/volt/v1/discovery_api.proto +39 -0
- package/protobuf/tdx/volt_api/volt/v1/file.proto +17 -0
- package/protobuf/tdx/volt_api/volt/v1/file_api.proto +117 -0
- package/protobuf/tdx/volt_api/volt/v1/remote.proto +94 -0
- package/protobuf/tdx/volt_api/volt/v1/status.proto +15 -0
- package/protobuf/tdx/volt_api/volt/v1/volt.proto +563 -0
- package/protobuf/tdx/volt_api/volt/v1/volt_api.proto +943 -0
- package/protobuf/tdx/volt_api/volt/v1/wire_api.proto +50 -0
- package/src/constants.js +75 -0
- package/src/grpc-call.js +254 -0
- package/src/grpc-utils.js +197 -0
- package/src/index.js +7 -0
- package/src/proto-utils.js +81 -0
- package/src/tunnel.pb.js +0 -0
- package/src/utils.js +105 -0
- package/src/volt-client-internal.js +288 -0
- package/src/volt-client.js +491 -0
- package/src/volt-connection.js +157 -0
- package/src/volt-crypto.js +213 -0
package/src/utils.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import ip from "ip";
|
|
2
|
+
import _ from "lodash";
|
|
3
|
+
import forge from "node-forge";
|
|
4
|
+
import bs58 from "bs58";
|
|
5
|
+
|
|
6
|
+
import {constants} from "./constants.js";
|
|
7
|
+
|
|
8
|
+
const {pki} = forge;
|
|
9
|
+
|
|
10
|
+
export const createSecureContextOptions = (cryptoOptions) => {
|
|
11
|
+
const tlsOptions = {
|
|
12
|
+
cert: Buffer.from(cryptoOptions.cert),
|
|
13
|
+
ca: Buffer.from(cryptoOptions.ca),
|
|
14
|
+
key: Buffer.from(cryptoOptions.key),
|
|
15
|
+
};
|
|
16
|
+
return tlsOptions;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const getServiceAddress = (mdnsService) => {
|
|
20
|
+
let address;
|
|
21
|
+
|
|
22
|
+
if (mdnsService.txtRecord && mdnsService.txtRecord.ip) {
|
|
23
|
+
// Use the advertised IP address if present.
|
|
24
|
+
address = `${mdnsService.txtRecord.ip}:${mdnsService.port}`;
|
|
25
|
+
} else {
|
|
26
|
+
// Find a valid ipv4 address.
|
|
27
|
+
const ipv4 = _.find(mdnsService.addresses, (addy) => ip.isV4Format(addy));
|
|
28
|
+
const serviceDetails = {
|
|
29
|
+
host: ipv4,
|
|
30
|
+
port: mdnsService.port,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
address = `${serviceDetails.host}:${serviceDetails.port}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return address;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const privateKeyToPem = (keyPair) => {
|
|
40
|
+
return pki.privateKeyToPem(keyPair.privateKey);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const publicKeyToPem = (keyPair) => {
|
|
44
|
+
return pki.publicKeyToPem(keyPair.publicKey);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const deserialisePEM = (pem) => {
|
|
48
|
+
return pem.replace(/\\r\\n/g, "\r\n");
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const removeCarriageReturn = (inp) => {
|
|
52
|
+
return inp.replace(/\r\n/g, "\n");
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const createBase58Hash = (inp) => {
|
|
56
|
+
const md = forge.md.sha256.create();
|
|
57
|
+
md.update(inp);
|
|
58
|
+
const hashBuf = Buffer.from(md.digest().getBytes(), "binary");
|
|
59
|
+
return bs58.encode(hashBuf);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const createPublicKeyFingerprint = (key) => {
|
|
63
|
+
// Remove carriage returns before creating hash to bring into line with openSSL / volt format.
|
|
64
|
+
return createBase58Hash(removeCarriageReturn(key));
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const signBase64 = (key, msg) => {
|
|
68
|
+
const md = forge.md.sha256.create();
|
|
69
|
+
md.update(msg);
|
|
70
|
+
const signature = key.sign(md);
|
|
71
|
+
return forge.util.encode64(signature);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const verifyBase64 = (key, msg, signature) => {
|
|
75
|
+
const md = forge.md.sha256.create();
|
|
76
|
+
md.update(msg);
|
|
77
|
+
return key.verify(md.digest().bytes(), forge.util.decode64(signature));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const getDIDResolutionURL = (didIn) => {
|
|
81
|
+
const did = didIn.toLowerCase().split(":");
|
|
82
|
+
if (did.length < 3) {
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (did[0] !== "did") {
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (did[1] !== "tdx") {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let hostName;
|
|
95
|
+
let didGUID;
|
|
96
|
+
if (did.length === 3) {
|
|
97
|
+
hostName = constants.defaultDIDHostName;
|
|
98
|
+
didGUID = did[2];
|
|
99
|
+
} else {
|
|
100
|
+
hostName = did[2];
|
|
101
|
+
didGUID = did[3];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return `https://${hostName}/api/identity/${didGUID}`;
|
|
105
|
+
};
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/* eslint-disable no-underscore-dangle */
|
|
2
|
+
import debug from "debug";
|
|
3
|
+
import ip from "ip";
|
|
4
|
+
import bent from "bent";
|
|
5
|
+
import forge from "node-forge";
|
|
6
|
+
import lodash from "lodash";
|
|
7
|
+
import VoltConnection from "./volt-connection.js";
|
|
8
|
+
import {getServiceDescriptors} from "./proto-utils.js";
|
|
9
|
+
import {createClient as createGrpcClient} from "./grpc-utils.js";
|
|
10
|
+
import * as voltUtils from "./utils.js";
|
|
11
|
+
import {constants} from "./constants.js";
|
|
12
|
+
import GRPCCall from "./grpc-call.js";
|
|
13
|
+
|
|
14
|
+
const {pki} = forge;
|
|
15
|
+
const {filter, pick} = lodash;
|
|
16
|
+
|
|
17
|
+
const log = debug("volt-client-grpc:volt-client-internal");
|
|
18
|
+
const voltServices = [
|
|
19
|
+
constants.serviceType.voltAPI,
|
|
20
|
+
constants.serviceType.fileAPI,
|
|
21
|
+
constants.serviceType.sqliteServerAPI,
|
|
22
|
+
constants.serviceType.ssiAPI,
|
|
23
|
+
constants.serviceType.tunnelAPI,
|
|
24
|
+
constants.serviceType.wireAPI,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function issueBind(bindRequest, ttl) {
|
|
28
|
+
// eslint-disable-next-line no-use-before-define
|
|
29
|
+
return unaryCall.call(this, "Bind", bindRequest).then((bindResponse) => {
|
|
30
|
+
log("got binding request response %j", bindResponse);
|
|
31
|
+
if (bindResponse.status && bindResponse.status.code) {
|
|
32
|
+
log("error in bind response: %s", bindResponse.status.message);
|
|
33
|
+
return Promise.reject(new Error(bindResponse.status.message));
|
|
34
|
+
} else {
|
|
35
|
+
switch (bindResponse.decision) {
|
|
36
|
+
case "POLICY_DECISION_PERMIT":
|
|
37
|
+
//
|
|
38
|
+
// The request status is permit => cache the bind response info.
|
|
39
|
+
//
|
|
40
|
+
|
|
41
|
+
// This is the certificate assigned to us by the volt.
|
|
42
|
+
this._crypto.cache.cert = bindResponse.cert;
|
|
43
|
+
|
|
44
|
+
// This is the signing CA used by the volt (don't ov)
|
|
45
|
+
this._crypto.cache.ca = bindResponse.chain;
|
|
46
|
+
|
|
47
|
+
// Resource id is assigned by the volt.
|
|
48
|
+
this._crypto.cache.client_id = bindResponse.identity_id;
|
|
49
|
+
this._crypto.saveCache();
|
|
50
|
+
break;
|
|
51
|
+
case "POLICY_DECISION_DENY":
|
|
52
|
+
log(">>>>>>>>>>>>>>> access request is DENIED <<<<<<<<<<<<<<<<<<<");
|
|
53
|
+
break;
|
|
54
|
+
case "POLICY_DECISION_PROMPT":
|
|
55
|
+
case "POLICY_DECISION_PENDING":
|
|
56
|
+
log("access pending approval - waiting...");
|
|
57
|
+
break;
|
|
58
|
+
default:
|
|
59
|
+
log("ignoring unknown bind descision %s", bindResponse.status);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return bindResponse.decision;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findDIDDocumentService(document, serviceType) {
|
|
69
|
+
if (!document.service) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const matches = filter(document.service, (svc) => svc.type === serviceType);
|
|
74
|
+
return matches;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function bindInternal() {
|
|
78
|
+
try {
|
|
79
|
+
// Build grpc client info.
|
|
80
|
+
if (this.isRemote) {
|
|
81
|
+
const getJSON = bent("json");
|
|
82
|
+
// @todo - currently Volt tunnel CA discovery is not secure.
|
|
83
|
+
const protocol =
|
|
84
|
+
this._config.tunnel_host?.startsWith("localhost:") || this._config.cloud_host?.startsWith("localhost:")
|
|
85
|
+
? "http"
|
|
86
|
+
: "https";
|
|
87
|
+
const tunnelURL = `${protocol}://${this._config.tunnel_host || this._config.cloud_host}/info`;
|
|
88
|
+
log("attempting to retrieve tunnel information from %s", tunnelURL);
|
|
89
|
+
const tunnelInfo = await getJSON(tunnelURL);
|
|
90
|
+
if (!tunnelInfo.ca_pem) {
|
|
91
|
+
throw new Error("Unable to fetch tunnel CA - cannot securely connect.");
|
|
92
|
+
} else {
|
|
93
|
+
log("Auto-fetched tunnel CA %s, remote address %s", tunnelInfo.ca_pem, tunnelInfo.local_address);
|
|
94
|
+
this._crypto.cache.tunnel_ca_pem = tunnelInfo.ca_pem;
|
|
95
|
+
this._crypto.cache.tunnel_address = tunnelInfo.local_address;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!this._crypto.cache.ca) {
|
|
100
|
+
this._crypto.cache.ca = this._options.ca_pem;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!this._crypto.cache.ca) {
|
|
104
|
+
throw new Error("No certificate authority found in configuration for target Volt - check configuration");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const keyPair = this._crypto.getKey();
|
|
108
|
+
const publicKeyPem = pki.publicKeyToPem(keyPair.publicKey);
|
|
109
|
+
|
|
110
|
+
if (!this._crypto.cache.bindIp) {
|
|
111
|
+
// Default the IP address if none is specified. This is used to set the SAN in the
|
|
112
|
+
// certificate issued by the Volt.
|
|
113
|
+
this._crypto.cache.bindIp = ip.address();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this._crypto.saveCache();
|
|
117
|
+
|
|
118
|
+
// Form the request message.
|
|
119
|
+
const bindRequest = {
|
|
120
|
+
binding_name: this._config.client_name,
|
|
121
|
+
public_key: publicKeyPem,
|
|
122
|
+
host: this._crypto.cache.bindIp,
|
|
123
|
+
x509_credential: [],
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (this._crypto.cache.cert) {
|
|
127
|
+
bindRequest.x509_credential = [this._crypto.cache.cert + this._crypto.cache.ca];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (this._options.challenge_code) {
|
|
131
|
+
bindRequest.challenge = voltUtils.signBase64(keyPair.privateKey, this._options.challenge_code);
|
|
132
|
+
} else {
|
|
133
|
+
log("****no Volt challenge code available**** => not sending challenge signature");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// For remote connections, add our cloud-issued certificate as an additional credential.
|
|
137
|
+
if (this._crypto.cache.tunnel_ca_pem && this._crypto.cache.cloud_cert) {
|
|
138
|
+
bindRequest.x509_credential = bindRequest.x509_credential.concat(
|
|
139
|
+
this._crypto.cache.cloud_cert + this._crypto.cache.tunnel_ca_pem
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
log("sending %d x509 credentials", bindRequest.x509_credential.length);
|
|
144
|
+
log("bind TTL is ", this._config.bindRequestTTL);
|
|
145
|
+
|
|
146
|
+
// This will block while the request is pending.
|
|
147
|
+
const pollInterval = 10000;
|
|
148
|
+
let decision = "";
|
|
149
|
+
do {
|
|
150
|
+
if (decision) {
|
|
151
|
+
// Wait a while before retrying.
|
|
152
|
+
// eslint-disable-next-line no-await-in-loop
|
|
153
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
154
|
+
}
|
|
155
|
+
// eslint-disable-next-line no-await-in-loop
|
|
156
|
+
decision = await issueBind.call(this, bindRequest, this._options.bindRequestTTL);
|
|
157
|
+
log("binding decision: %s", decision);
|
|
158
|
+
} while (decision === "POLICY_DECISION_PROMPT" || decision === "POLICY_DECISION_PENDING");
|
|
159
|
+
|
|
160
|
+
return decision === "POLICY_DECISION_PERMIT";
|
|
161
|
+
} catch (err) {
|
|
162
|
+
log("failure binding to Volt [%s]", err.message);
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function connectInternal() {
|
|
168
|
+
try {
|
|
169
|
+
if (this._voltConnection) {
|
|
170
|
+
log("connectInternal - Volt connection already exists");
|
|
171
|
+
return Promise.resolve();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const onDisconnected = () => {
|
|
175
|
+
this._connected = false;
|
|
176
|
+
this._session = null;
|
|
177
|
+
|
|
178
|
+
if (this._cachedClient) {
|
|
179
|
+
this._grpc.closeClient(this._cachedClient);
|
|
180
|
+
this._cachedClient = null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (this._cachedRemoteClient) {
|
|
184
|
+
this._grpc.closeClient(this._cachedRemoteClient);
|
|
185
|
+
this._cachedRemoteClient = null;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
this._voltConnection = new VoltConnection(this, this._grpc, this._options);
|
|
190
|
+
|
|
191
|
+
this._voltConnection.on("connected", (session) => {
|
|
192
|
+
this._session = session;
|
|
193
|
+
this._connected = !!session;
|
|
194
|
+
if (!this._connected) {
|
|
195
|
+
onDisconnected();
|
|
196
|
+
}
|
|
197
|
+
this.emit("connected", !!session);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
this._voltConnection.on("error", (err) => {
|
|
201
|
+
// Should auto-reconnect.
|
|
202
|
+
log("Volt connection error [%s]", err.message);
|
|
203
|
+
if (this._connected) {
|
|
204
|
+
onDisconnected();
|
|
205
|
+
this.emit("connected", false);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
this._voltConnection.on("evt", (evt) => {
|
|
210
|
+
this.emit("evt", evt);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return this._voltConnection.connect();
|
|
214
|
+
} catch (err) {
|
|
215
|
+
log("connect - error [%s]", err.message);
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function getVoltAPIClientInternal() {
|
|
221
|
+
if (!this._cachedClient) {
|
|
222
|
+
const serviceAddress = this.isRemote ? this._crypto.cache.tunnel_address : this._options.local_address;
|
|
223
|
+
log("creating cached VoltAPI service client on %s", serviceAddress);
|
|
224
|
+
let serviceDescriptors = {};
|
|
225
|
+
voltServices.forEach((pkg) => {
|
|
226
|
+
const packageDescriptors = getServiceDescriptors(this._grpc, pkg);
|
|
227
|
+
serviceDescriptors = {...serviceDescriptors, ...packageDescriptors};
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
this._cachedClient = createGrpcClient(
|
|
231
|
+
this._grpc,
|
|
232
|
+
serviceDescriptors,
|
|
233
|
+
serviceAddress,
|
|
234
|
+
this._crypto.cache,
|
|
235
|
+
this.isRemote && !this.isVoltTunnel,
|
|
236
|
+
this._config.cloud_host ? this._options.id : "",
|
|
237
|
+
this._config.cloud_host ? this._options.id : ""
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return this._cachedClient;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function unaryCall(method, request) {
|
|
245
|
+
const grpcClient = this.getVoltAPIClient();
|
|
246
|
+
|
|
247
|
+
const call = new GRPCCall(this, method, "METHOD_TYPE_UNARY");
|
|
248
|
+
call.start(grpcClient, request);
|
|
249
|
+
return call.wait();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function fetchVoltConfig(volt_did) {
|
|
253
|
+
const didResolution = voltUtils.getDIDResolutionURL(volt_did);
|
|
254
|
+
if (!didResolution) {
|
|
255
|
+
throw new Error(`Invalid Volt DID: ${volt_did}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const getJSON = bent("json");
|
|
259
|
+
const didDocument = await getJSON(didResolution);
|
|
260
|
+
const voltConfigServices = findDIDDocumentService(didDocument, constants.didServiceType.voltConfig);
|
|
261
|
+
if (voltConfigServices.length !== 1) {
|
|
262
|
+
log(
|
|
263
|
+
"Unexpected number of Volt configuration endpoints found in DID document for %s, services found: %d",
|
|
264
|
+
volt_did,
|
|
265
|
+
voltConfigServices.length
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
const serviceEndpoint = voltConfigServices[0].serviceEndpoint;
|
|
269
|
+
const resolvedConfig = await getJSON(serviceEndpoint);
|
|
270
|
+
|
|
271
|
+
const voltConfig = pick(
|
|
272
|
+
resolvedConfig,
|
|
273
|
+
"id",
|
|
274
|
+
"display_name",
|
|
275
|
+
"public_key",
|
|
276
|
+
"fingerprint",
|
|
277
|
+
"local_address",
|
|
278
|
+
"ca_pem",
|
|
279
|
+
"challenge_code"
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
volt: voltConfig,
|
|
284
|
+
tunnel_ca_pem: resolvedConfig.tunnel_ca_pem,
|
|
285
|
+
tunnel_address: resolvedConfig.tunnel_address,
|
|
286
|
+
cloud_host: resolvedConfig.tunnel_address.split(":")[0],
|
|
287
|
+
};
|
|
288
|
+
}
|