@tdxvolt/volt-client-grpc 0.15.1 → 0.16.0-beta.10

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.
@@ -16,6 +16,7 @@ import * as voltUtils from "./utils.js";
16
16
  import { constants } from "./constants.js";
17
17
  import GRPCCall from "./grpc-call.js";
18
18
  import RpcInvocation from "./rpc-invocation.js";
19
+ import { v4 as generateId } from "uuid";
19
20
 
20
21
  const { pki } = forge;
21
22
  const { filter, pick } = lodash;
@@ -31,30 +32,31 @@ const voltServices = [
31
32
  constants.serviceType.wireAPI
32
33
  ];
33
34
 
34
- function issueBind(bindRequest, ttl) {
35
+ function issueAuthenticate(authenticateRequest, ttl) {
35
36
  // eslint-disable-next-line no-use-before-define
36
37
  return unaryCallInternal
37
- .call(this, "Bind", bindRequest)
38
- .then((bindResponse) => {
39
- log("got binding request response %j", bindResponse);
40
- if (bindResponse.status?.code) {
41
- log("error in bind response: %s", bindResponse.status.message);
42
- return Promise.reject(new Error(bindResponse.status.message));
38
+ .call(this, "Authenticate", authenticateRequest)
39
+ .then((authenticateResponse) => {
40
+ log("got authenticate response %j", authenticateResponse);
41
+ if (authenticateResponse.status?.code) {
42
+ log("error in bind response: %s", authenticateResponse.status.message);
43
+ return Promise.reject(new Error(authenticateResponse.status.message));
43
44
  } else {
44
- switch (bindResponse.decision) {
45
+ switch (authenticateResponse.decision) {
45
46
  case "POLICY_DECISION_PERMIT": {
46
47
  //
47
48
  // The request status is permit => cache the bind response info.
48
49
  //
49
50
 
50
51
  // This is the certificate assigned to us by the volt.
51
- this._credential.cache.cert = bindResponse.cert;
52
+ this._credential.cache.cert = authenticateResponse.cert;
52
53
 
53
54
  // This is the signing CA used by the volt.
54
- this._credential.cache.ca = bindResponse.chain;
55
+ this._credential.cache.ca = authenticateResponse.chain;
55
56
 
56
57
  // Identity resource id is assigned by the volt.
57
- this._credential.cache.client_id = bindResponse.identity_id;
58
+ this._credential.cache.session_id = authenticateResponse.session_id;
59
+
58
60
  this._credential.saveCache();
59
61
  break;
60
62
  }
@@ -68,12 +70,15 @@ function issueBind(bindRequest, ttl) {
68
70
  break;
69
71
  }
70
72
  default:
71
- log("ignoring unknown bind descision %s", bindResponse.status);
73
+ log(
74
+ "ignoring unknown bind descision %s",
75
+ authenticateResponse.status
76
+ );
72
77
  break;
73
78
  }
74
79
  }
75
80
 
76
- return bindResponse.decision;
81
+ return authenticateResponse.decision;
77
82
  });
78
83
  }
79
84
 
@@ -86,7 +91,7 @@ function findDIDDocumentService(document, serviceType) {
86
91
  return matches;
87
92
  }
88
93
 
89
- export async function bindInternal() {
94
+ export async function authenticateInternal(useDID = true) {
90
95
  try {
91
96
  // Check if we need to resolve a Relay volt.
92
97
  if (
@@ -100,6 +105,7 @@ export async function bindInternal() {
100
105
  this,
101
106
  `${relayURL}/discovery`
102
107
  );
108
+
103
109
  if (this._voltConfig.relay.ca_pem) {
104
110
  log(
105
111
  "Auto-fetched Relay CA %s, remote address %s",
@@ -122,7 +128,9 @@ export async function bindInternal() {
122
128
  }
123
129
 
124
130
  const keyPair = this._credential.getKey();
125
- const publicKeyPem = pki.publicKeyToPem(keyPair.publicKey);
131
+ const publicKeyPem = voltUtils.removeCarriageReturn(
132
+ pki.publicKeyToPem(keyPair.publicKey)
133
+ );
126
134
 
127
135
  if (!this._credential.cache.bindIp) {
128
136
  // Default the IP address if none is specified. This is used to set the SAN in the
@@ -133,21 +141,13 @@ export async function bindInternal() {
133
141
  this._credential.saveCache();
134
142
 
135
143
  // Form the request message.
136
- const bindRequest = {
144
+ const authenticateRequest = {
137
145
  binding_name: this._config.client_name,
138
- public_key: publicKeyPem,
139
- host: this._credential.cache.bindIp,
140
- x509_credential: []
146
+ host: this._credential.cache.bindIp
141
147
  };
142
148
 
143
- if (this._credential.cache.cert) {
144
- bindRequest.x509_credential = [
145
- this._credential.cache.cert + this._credential.cache.ca
146
- ];
147
- }
148
-
149
149
  if (this._voltConfig.challenge_code) {
150
- bindRequest.challenge = voltUtils.signBase64(
150
+ authenticateRequest.challenge = voltUtils.signBase64(
151
151
  keyPair.privateKey,
152
152
  this._voltConfig.challenge_code
153
153
  );
@@ -157,15 +157,71 @@ export async function bindInternal() {
157
157
  );
158
158
  }
159
159
 
160
- // For remote connections, add our cloud-issued certificate as an additional credential.
161
- if (this._voltConfig?.relay?.ca_pem && this._credential.cache.cloud_cert) {
162
- bindRequest.x509_credential = bindRequest.x509_credential.concat(
163
- this._credential.cache.cloud_cert + this._voltConfig.relay.ca_pem
160
+ if (!useDID) {
161
+ authenticateRequest.public_key = publicKeyPem;
162
+ } else if (!this._credential.cache.identity_id) {
163
+ // If we don't have an identity id, then we need to create a DID.
164
+ this._credential.cache.identity_id = `did:volt:${generateId()}`;
165
+
166
+ const did = this._credential.cache.identity_id;
167
+
168
+ // Order of properties in the DID document is important,
169
+ // they must be in alphabetical order throughout the document.
170
+ // This is because the DID document is signed and the signature
171
+ // is over the entire document.
172
+ const didDocument = {
173
+ "@context": "https://www.w3.org/ns/did/v1",
174
+ authentication: [`${did}#key-1`],
175
+ description: this._config.client_name,
176
+ id: did,
177
+ verificationMethod: [
178
+ {
179
+ controller: did,
180
+ id: `${did}#key-1`,
181
+ publicKeyPem: publicKeyPem,
182
+ type: "RsaSignature2018"
183
+ }
184
+ ]
185
+ };
186
+
187
+ const signDocument = JSON.stringify(didDocument, null, 2);
188
+ log("signing did document");
189
+ log(signDocument);
190
+
191
+ // Sign the DID document.
192
+ const documentDigest = voltUtils.signBase64(
193
+ keyPair.privateKey,
194
+ signDocument
164
195
  );
196
+
197
+ const proofSignature = voltUtils.jwsSignature(documentDigest);
198
+
199
+ // Create the DID document proof.
200
+ didDocument.proof = {
201
+ created: new Date().toISOString(),
202
+ jws: proofSignature,
203
+ proofPurpose: "assertionMethod",
204
+ type: "RsaSignature2018",
205
+ verificationMethod: `${did}#key-1`
206
+ };
207
+
208
+ // Set the DID document with proof in the authenticate request.
209
+ authenticateRequest.did_document = JSON.stringify(didDocument, null, 2);
210
+
211
+ // Now sign the entire DID document, including the proof and set it in the
212
+ // bind request. This is required by the Volt to verify that we have the private key for the DID.
213
+ authenticateRequest.did_document_signature = voltUtils.signBase64(
214
+ keyPair.privateKey,
215
+ authenticateRequest.did_document
216
+ );
217
+
218
+ log("did document is %s", authenticateRequest.did_document);
219
+ log("did signature is %s", authenticateRequest.did_document_signature);
220
+ } else {
221
+ authenticateRequest.did = this._credential.cache.identity_id;
165
222
  }
166
223
 
167
- log("sending %d x509 credentials", bindRequest.x509_credential.length);
168
- log("bind TTL is ", this._config.bindRequestTTL);
224
+ log("authenticate TTL is ", this._config.bindRequestTTL);
169
225
 
170
226
  // This will block while the request is pending.
171
227
  const pollInterval = 10000;
@@ -177,12 +233,12 @@ export async function bindInternal() {
177
233
  await new Promise((resolve) => setTimeout(resolve, pollInterval));
178
234
  }
179
235
  // eslint-disable-next-line no-await-in-loop
180
- decision = await issueBind.call(
236
+ decision = await issueAuthenticate.call(
181
237
  this,
182
- bindRequest,
238
+ authenticateRequest,
183
239
  this._voltConfig.bindRequestTTL
184
240
  );
185
- log("binding decision: %s", decision);
241
+ log("authenticate decision: %s", decision);
186
242
  } while (
187
243
  decision === "POLICY_DECISION_PROMPT" ||
188
244
  decision === "POLICY_DECISION_PENDING"
@@ -235,6 +291,15 @@ export function connectInternal(helloPayload) {
235
291
  onDisconnected();
236
292
  this.emit("connected", false);
237
293
  }
294
+
295
+ if (err.message.endsWith("session invalid")) {
296
+ log("Volt session invalid - clearing session");
297
+ delete this._credential.cache.session_id;
298
+ delete this._credential.cache.cert;
299
+ this._credential.saveCache();
300
+ }
301
+
302
+ this.emit("connectionError", err);
238
303
  });
239
304
 
240
305
  this._voltConnection.on("evt", (evt) => {
@@ -304,9 +369,6 @@ export function getVoltAPIClientInternal() {
304
369
 
305
370
  export function getAPIClientInternal(service) {
306
371
  if (!this._cachedService[service.id]) {
307
- const isRelayedService =
308
- service.service_description.host_type === "SERVICE_HOST_TYPE_RELAYED";
309
-
310
372
  const serviceAddress = this.isRemote
311
373
  ? this._voltConfig.relay.address
312
374
  : service.service_description.host_address;
@@ -354,9 +416,8 @@ export function getServiceClient(service) {
354
416
  }
355
417
 
356
418
  export function unaryCallInternal(method, request, service) {
357
- const grpcClient = getServiceClient.call(this, service);
358
-
359
419
  return new Promise((resolve, reject) => {
420
+ const grpcClient = getServiceClient.call(this, service);
360
421
  const call = new GRPCCall(this, method, "METHOD_TYPE_UNARY", service);
361
422
 
362
423
  let response = null;
@@ -5,7 +5,7 @@ import path from "path";
5
5
  import { getServiceDescriptors } from "./proto-utils.js";
6
6
  import { createClient as createGrpcClient } from "./grpc-utils.js";
7
7
  import {
8
- bindInternal,
8
+ authenticateInternal,
9
9
  connectInternal,
10
10
  fetchVoltConfig,
11
11
  fetchVoltConfigFromDID,
@@ -166,14 +166,14 @@ export class VoltClient extends EventEmitter {
166
166
  if (this._credential._cryptoCache.cert) {
167
167
  isBound = true;
168
168
  } else {
169
- // No certificate present in the cache yet => we need to bind.
169
+ // No certificate present in the cache yet => we need to authenticate.
170
170
  try {
171
- isBound = await bindInternal.call(this);
171
+ isBound = await authenticateInternal.call(this);
172
172
  } catch (bindErr) {
173
173
  if (bindErr.message === "invalid arguments") {
174
174
  // This is usually the result of a missing challenge signature or credential.
175
175
  log(
176
- "failure binding to Volt - did you supply a challenge code or verified credential?"
176
+ "failure authenticating on Volt - did you supply a challenge code or verified credential?"
177
177
  );
178
178
  throw bindErr;
179
179
  }
@@ -182,7 +182,7 @@ export class VoltClient extends EventEmitter {
182
182
 
183
183
  if (!isBound) {
184
184
  // This is usually because of request being denied.
185
- throw new Error("Volt binding failed");
185
+ throw new Error("Volt authentication failed");
186
186
  }
187
187
 
188
188
  return Promise.resolve();
@@ -408,6 +408,10 @@ export class VoltClient extends EventEmitter {
408
408
  * VoltAPI - volt management
409
409
  */
410
410
 
411
+ Authenticate(request) {
412
+ return unaryCallInternal.call(this, "Authenticate", request);
413
+ }
414
+
411
415
  Bind(request) {
412
416
  return unaryCallInternal.call(this, "Bind", request);
413
417
  }
@@ -19,10 +19,9 @@ function _cleanUp(immediateReconnect) {
19
19
  this._pingTimeoutTimer = 0;
20
20
  }
21
21
 
22
- if (
23
- !this._dying &&
24
- (immediateReconnect || (this._autoRetry && !this._reconnectTimer))
25
- ) {
22
+ if (this._dying) {
23
+ clearTimeout(this._reconnectTimer);
24
+ } else if (immediateReconnect || (this._autoRetry && !this._reconnectTimer)) {
26
25
  this._reconnectTimer = setTimeout(
27
26
  () => {
28
27
  this._reconnectTimer = 0;
@@ -69,8 +69,16 @@ export class VoltCredential {
69
69
  return this._cryptoCache;
70
70
  }
71
71
 
72
- get clientId() {
73
- return this._cryptoCache.client_id;
72
+ get certificate() {
73
+ return this._cryptoCache.cert;
74
+ }
75
+
76
+ get sessionId() {
77
+ return this._cryptoCache.session_id;
78
+ }
79
+
80
+ get identityId() {
81
+ return this._cryptoCache.identity_id;
74
82
  }
75
83
 
76
84
  get voltPublicKey() {
@@ -159,7 +167,7 @@ export class VoltCredential {
159
167
  aud: audience,
160
168
  iat: Math.floor(Date.now() / 1000) - ttl,
161
169
  exp: Math.floor(Date.now() / 1000) + ttl,
162
- sub: this._cryptoCache.client_id
170
+ sub: this._cryptoCache.session_id
163
171
  };
164
172
 
165
173
  let sharedKey;
@@ -241,12 +249,13 @@ export class VoltCredential {
241
249
  }
242
250
 
243
251
  static rsaEncrypt(key, buffer) {
244
- return crypto.publicEncrypt(key, buffer).toString("base64");
252
+ const cipher = crypto.publicEncrypt(key, buffer);
253
+ return cipher.toString("base64url");
245
254
  }
246
255
 
247
256
  static rsaDecrypt(key, buffer) {
248
257
  const keyObj = crypto.createPrivateKey(key);
249
- const cipher = Buffer.from(buffer, "base64");
258
+ const cipher = Buffer.from(buffer, "base64url");
250
259
  return Buffer.from(crypto.privateDecrypt(keyObj, cipher));
251
260
  }
252
261
  }
@@ -1,37 +0,0 @@
1
- syntax = "proto3";
2
-
3
- package tdx.volt_api.ssi.v1;
4
-
5
- import "tdx/volt_api/volt/v1/status.proto";
6
-
7
- // Volt DIDs are essentially 'peer DIDs', i.e. they are not globally resolvable.
8
- // TDX DIDs are globally resolvable.
9
-
10
- service SsiAPI {
11
- rpc ResolveDID(ResolveDIDRequest) returns (ResolveDIDResponse);
12
- rpc TrustVerifiableCredential(TrustVerifiableCredentialRequest) returns (TrustVerifiableCredentialResponse);
13
- }
14
-
15
- //
16
- // Do we need to support/differentiate between plain DIDs and DID urls here?
17
- // Where a DID url is a DID followed by some additional lookup info,
18
- // e.g. did:tdx:volt:92181ffa-072a-452a-ae79-a243ab2812fc;service=/some/service?query#fragment
19
- //
20
- message ResolveDIDRequest {
21
- string did_url = 1;
22
- }
23
-
24
- message ResolveDIDResponse {
25
- tdx.volt_api.volt.v1.Status status = 1;
26
- string did_document_json = 2;
27
- }
28
-
29
- // Empty ATM - a VC is issued for the authenticated identity.
30
- message TrustVerifiableCredentialRequest {
31
- }
32
-
33
- message TrustVerifiableCredentialResponse {
34
- tdx.volt_api.volt.v1.Status status = 1;
35
- string verifiable_credential_json = 2;
36
- }
37
-