Skip to content

Commit e7718d5

Browse files
elizabethhealypflynn-virtrujakedoublevopentdf-automation[bot]dependabot[bot]
authored
feat(sdk): Move to rewrap v2 request/response format (#774)
* feat: Certificates & Obligations (#755) * - Pin `@bufbuild/buf` and `@bufbuild/protoc-gen-es` dependencies to specific versions. - Update copyright notices in `http_pb.ts` and `validate_pb.ts`. * - Pin `@bufbuild/buf` and `@bufbuild/protoc-gen-es` dependencies to specific versions. - Update copyright notices in `http_pb.ts` and `validate_pb.ts`. * Add `obligations` and `rootCerts` attributes to test fixtures and mock data. Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * feat: upgrade tdf clients to rewrap v2 proto structure Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * updates to match go behavior Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * feat: Get Namespace (#756) * - Pin `@bufbuild/buf` and `@bufbuild/protoc-gen-es` dependencies to specific versions. - Update copyright notices in `http_pb.ts` and `validate_pb.ts`. * - Pin `@bufbuild/buf` and `@bufbuild/protoc-gen-es` dependencies to specific versions. - Update copyright notices in `http_pb.ts` and `validate_pb.ts`. * Add `obligations` and `rootCerts` attributes to test fixtures and mock data. * Add `getRootCertsFromNamespace` function and include headers initialization in `authProvider` * Add input validation for `getRootCertsFromNamespace` and basic unit tests Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * feat(sdk): initial obligations support in rewrap flow (#748) * feat(core): initial obligations support in rewrap flow * 🤖 🎨 Autoformat Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> * wip Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> * more wip * rm unused import * 🤖 🎨 Autoformat Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> * lint fix Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> * tests Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> * 🤖 🎨 Autoformat Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> * move file * tdf3 client * cleanup * obligations method on opentdf reader classes * requiredObligations on DecoratedReadableStream in tdf3 * wip: fetch decision if obligations haven't been set on reader * wip * 🤖 🎨 Autoformat Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> * bugfix in case of no data attributes leading to no obligations Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> * working state Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> * fix Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> * 🤖 🎨 Autoformat Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> * fix comments * rm example web app hardcoded attributes and obligations * unit tests for getRequiredObligations * improve nullish operators * cleanup * 🤖 🎨 Autoformat Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> * improvements * fix * 🤖 🎨 Autoformat Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> * improve log * put back package.json changes * pr feedback Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> * 🤖 🎨 Autoformat Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> * rm rewrap header for obligations over legacy http for older platforms --------- Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * chore: release sdk 0.5.0 (#658) * chore(main): release sdk 0.5.0 * Update dependencies --------- Co-authored-by: opentdf-automation[bot] <149537512+opentdf-automation[bot]@users.noreply.github.com> Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * feat(ci): Add a workflow to update the generated code for new protocol/go versions (#767) * add a workflow to update the pbs * trigger on PR * correct platform location * add gh token to env * remove extra file after use * detect changes on regen * test with latest version * remove, test changes * test for signed commits * try with api * push the new branch * use a shorter file name in the message * fix for non existing files * run slightly after midnight to avoid queues Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * 🤖 🎨 Autoformat Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * handle rewrap response Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * formatting Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * passing unit tests Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * format Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * chore(docs): bump playwright and @playwright/test in /web-app/tests (#763) Bumps [playwright](https://github.com/microsoft/playwright) to 1.56.1 and updates ancestor dependency [@playwright/test](https://github.com/microsoft/playwright). These dependencies need to be updated together. Updates `playwright` from 1.50.1 to 1.56.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](microsoft/playwright@v1.50.1...v1.56.1) Updates `@playwright/test` from 1.50.1 to 1.56.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](microsoft/playwright@v1.50.1...v1.56.1) --- updated-dependencies: - dependency-name: playwright dependency-version: 1.56.1 dependency-type: indirect - dependency-name: "@playwright/test" dependency-version: 1.56.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * chore(docs): bump vite from 6.3.6 to 6.4.1 in /web-app (#764) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.6 to 6.4.1. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 6.4.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * v1 backwards compatability Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * error handling Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * cleanup Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * chore(docs): bump playwright and @playwright/test in /web-app (#775) Bumps [playwright](https://github.com/microsoft/playwright) and [@playwright/test](https://github.com/microsoft/playwright). These dependencies needed to be updated together. Updates `playwright` from 1.50.1 to 1.56.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](microsoft/playwright@v1.50.1...v1.56.1) Updates `@playwright/test` from 1.50.1 to 1.56.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](microsoft/playwright@v1.50.1...v1.56.1) --- updated-dependencies: - dependency-name: playwright dependency-version: 1.56.1 dependency-type: direct:development - dependency-name: "@playwright/test" dependency-version: 1.56.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: Elizabeth Healy <ehealy@virtru.com> * suggestions --------- Signed-off-by: Elizabeth Healy <ehealy@virtru.com> Signed-off-by: jakedoublev <jake.vanvorhis@virtru.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Paul Flynn <43211074+pflynn-virtru@users.noreply.github.com> Co-authored-by: jakedoublev <jake.vanvorhis@virtru.com> Co-authored-by: Jake Van Vorhis <83739412+jakedoublev@users.noreply.github.com> Co-authored-by: opentdf-automation[bot] <149537512+opentdf-automation[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent da9f2da commit e7718d5

File tree

5 files changed

+341
-90
lines changed

5 files changed

+341
-90
lines changed

lib/src/access/access-rpc.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ import {
88
} from '../access.js';
99

1010
import { type AuthProvider } from '../auth/auth.js';
11-
import { ConfigurationError, NetworkError } from '../errors.js';
11+
import {
12+
ConfigurationError,
13+
InvalidFileError,
14+
NetworkError,
15+
PermissionDeniedError,
16+
ServiceError,
17+
UnauthenticatedError,
18+
} from '../errors.js';
1219
import { PlatformClient } from '../platform.js';
1320
import { RewrapResponse } from '../platform/kas/kas_pb.js';
1421
import { ListKeyAccessServersResponse } from '../platform/policy/kasregistry/key_access_server_registry_pb.js';
@@ -19,6 +26,7 @@ import {
1926
validateSecureUrl,
2027
} from '../utils.js';
2128
import { X_REWRAP_ADDITIONAL_CONTEXT } from './constants.js';
29+
import { ConnectError, Code } from '@connectrpc/connect';
2230

2331
/**
2432
* Get a rewrapped access key to the document, if possible
@@ -42,11 +50,66 @@ export async function fetchWrappedKey(
4250
[X_REWRAP_ADDITIONAL_CONTEXT]: rewrapAdditionalContextHeader,
4351
};
4452
}
53+
let response: RewrapResponse;
4554
try {
46-
return await platform.v1.access.rewrap({ signedRequestToken }, options);
55+
response = await platform.v1.access.rewrap({ signedRequestToken }, options);
4756
} catch (e) {
48-
throw new NetworkError(`[${platformUrl}] [Rewrap] ${extractRpcErrorMessage(e)}`);
57+
handleRpcRewrapError(e, platformUrl);
58+
}
59+
return response;
60+
}
61+
62+
export function handleRpcRewrapError(e: unknown, platformUrl: string): never {
63+
if (e instanceof ConnectError) {
64+
console.log('Error is a ConnectError with code:', e.code);
65+
switch (e.code) {
66+
case Code.InvalidArgument: // 400 Bad Request
67+
throw new InvalidFileError(`400 for [${platformUrl}]: rewrap bad request [${e.message}]`);
68+
case Code.PermissionDenied: // 403 Forbidden
69+
throw new PermissionDeniedError(`403 for [${platformUrl}]; rewrap permission denied`);
70+
case Code.Unauthenticated: // 401 Unauthorized
71+
throw new UnauthenticatedError(`401 for [${platformUrl}]; rewrap auth failure`);
72+
case Code.Internal:
73+
case Code.Unimplemented:
74+
case Code.DataLoss:
75+
case Code.Unknown:
76+
case Code.DeadlineExceeded:
77+
case Code.Unavailable: // >=500 Server Error
78+
throw new ServiceError(
79+
`${e.code} for [${platformUrl}]: rewrap failure due to service error [${e.message}]`
80+
);
81+
default:
82+
throw new NetworkError(`[${platformUrl}] [Rewrap] ${e.message}`);
83+
}
84+
}
85+
throw new NetworkError(`[${platformUrl}] [Rewrap] ${extractRpcErrorMessage(e)}`);
86+
}
87+
88+
export function handleRpcRewrapErrorString(e: string, platformUrl: string): never {
89+
if (e.includes(Code[Code.InvalidArgument])) {
90+
// 400 Bad Request
91+
throw new InvalidFileError(`400 for [${platformUrl}]: rewrap bad request [${e}]`);
92+
}
93+
if (e.includes(Code[Code.PermissionDenied])) {
94+
// 403 Forbidden
95+
throw new PermissionDeniedError(`403 for [${platformUrl}]; rewrap permission denied`);
96+
}
97+
if (e.includes(Code[Code.Unauthenticated])) {
98+
// 401 Unauthorized
99+
throw new UnauthenticatedError(`401 for [${platformUrl}]; rewrap auth failure`);
100+
}
101+
if (
102+
e.includes(Code[Code.Internal]) ||
103+
e.includes(Code[Code.Unimplemented]) ||
104+
e.includes(Code[Code.DataLoss]) ||
105+
e.includes(Code[Code.Unknown]) ||
106+
e.includes(Code[Code.DeadlineExceeded]) ||
107+
e.includes(Code[Code.Unavailable])
108+
) {
109+
// >=500
110+
throw new ServiceError(`500+ [${platformUrl}]: rewrap failure due to service error [${e}]`);
49111
}
112+
throw new NetworkError(`[${platformUrl}] [Rewrap] ${e}`);
50113
}
51114

52115
export async function fetchKeyAccessServers(

lib/src/nanotdf/Client.ts

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import * as base64 from '../encodings/base64.js';
1+
import { create, toJsonString } from '@bufbuild/protobuf';
2+
import {
3+
UnsignedRewrapRequest_WithPolicyRequestSchema,
4+
UnsignedRewrapRequestSchema,
5+
} from '../platform/kas/kas_pb.js';
26
import { generateKeyPair, keyAgreement } from '../nanotdf-crypto/index.js';
37
import getHkdfSalt from './helpers/getHkdfSalt.js';
48
import DefaultParams from './models/DefaultParams.js';
@@ -8,13 +12,16 @@ import {
812
KasPublicKeyInfo,
913
OriginAllowList,
1014
} from '../access.js';
15+
import { handleRpcRewrapErrorString } from '../../src/access/access-rpc.js';
1116
import { AuthProvider, isAuthProvider, reqSignature } from '../auth/providers.js';
1217
import { ConfigurationError, DecryptError, TdfError, UnsafeUrlError } from '../errors.js';
1318
import {
1419
cryptoPublicToPem,
1520
getRequiredObligationFQNs,
1621
pemToCryptoPublicKey,
22+
upgradeRewrapResponseV1,
1723
validateSecureUrl,
24+
getPlatformUrlFromKasEndpoint,
1825
} from '../utils.js';
1926

2027
export interface ClientConfig {
@@ -260,18 +267,35 @@ export default class Client {
260267
throw new ConfigurationError('Signer key has not been set or generated');
261268
}
262269

263-
const requestBodyStr = JSON.stringify({
264-
algorithm: DefaultParams.defaultECAlgorithm,
265-
// nano keyAccess minimum, header is used for nano
270+
const unsignedRequest = create(UnsignedRewrapRequestSchema, {
271+
clientPublicKey: await cryptoPublicToPem(ephemeralKeyPair.publicKey),
272+
requests: [
273+
create(UnsignedRewrapRequest_WithPolicyRequestSchema, {
274+
keyAccessObjects: [
275+
{
276+
keyAccessObjectId: 'kao-0',
277+
keyAccessObject: {
278+
header: new Uint8Array(nanoTdfHeader),
279+
kasUrl: '',
280+
protocol: Client.KAS_PROTOCOL,
281+
keyType: Client.KEY_ACCESS_REMOTE,
282+
},
283+
},
284+
],
285+
algorithm: DefaultParams.defaultECAlgorithm,
286+
}),
287+
],
266288
keyAccess: {
267-
type: Client.KEY_ACCESS_REMOTE,
268-
url: '',
289+
header: new Uint8Array(nanoTdfHeader),
290+
kasUrl: '',
269291
protocol: Client.KAS_PROTOCOL,
270-
header: base64.encodeArrayBuffer(nanoTdfHeader),
292+
keyType: Client.KEY_ACCESS_REMOTE,
271293
},
272-
clientPublicKey: await cryptoPublicToPem(ephemeralKeyPair.publicKey),
294+
algorithm: DefaultParams.defaultECAlgorithm,
273295
});
274296

297+
const requestBodyStr = toJsonString(UnsignedRewrapRequestSchema, unsignedRequest);
298+
275299
const jwtPayload = { requestBody: requestBodyStr };
276300

277301
const signedRequestToken = await reqSignature(jwtPayload, requestSignerKeyPair.privateKey, {
@@ -285,9 +309,28 @@ export default class Client {
285309
this.authProvider,
286310
this.fulfillableObligationFQNs
287311
);
312+
upgradeRewrapResponseV1(rewrapResp);
313+
314+
// Assume only one response and one result for now (V1 style)
315+
const result = rewrapResp.responses[0].results[0];
316+
let entityWrappedKey: Uint8Array<ArrayBufferLike>;
317+
switch (result.result.case) {
318+
case 'kasWrappedKey': {
319+
entityWrappedKey = result.result.value;
320+
break;
321+
}
322+
case 'error': {
323+
handleRpcRewrapErrorString(
324+
result.result.value,
325+
getPlatformUrlFromKasEndpoint(kasRewrapUrl)
326+
);
327+
}
328+
default: {
329+
throw new DecryptError('KAS rewrap response missing wrapped key');
330+
}
331+
}
288332

289333
// Extract the iv and ciphertext
290-
const entityWrappedKey = rewrapResp.entityWrappedKey;
291334
const ivLength =
292335
clientVersion == Client.SDK_INITIAL_RELEASE ? Client.INITIAL_RELEASE_IV_SIZE : Client.IV_SIZE;
293336
const iv = entityWrappedKey.subarray(0, ivLength);

lib/src/utils.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { exportSPKI, importX509 } from 'jose';
33
import { base64 } from './encodings/index.js';
44
import { pemCertToCrypto, pemPublicToCrypto } from './nanotdf-crypto/pemPublicToCrypto.js';
55
import { ConfigurationError } from './errors.js';
6-
import { RewrapResponse } from './platform/kas/kas_pb.js';
6+
import {
7+
RewrapResponse,
8+
PolicyRewrapResultSchema,
9+
KeyAccessRewrapResultSchema,
10+
} from './platform/kas/kas_pb.js';
11+
import { create } from '@bufbuild/protobuf';
712
import { ConnectError } from '@connectrpc/connect';
813

914
const REQUIRED_OBLIGATIONS_METADATA_KEY = 'X-Required-Obligations';
@@ -255,3 +260,31 @@ export function getRequiredObligationFQNs(response: RewrapResponse) {
255260

256261
return [...requiredObligations.values()];
257262
}
263+
264+
/**
265+
* Upgrades a RewrapResponse from v1 format to v2.
266+
*/
267+
export function upgradeRewrapResponseV1(response: RewrapResponse) {
268+
if (response.responses.length > 0) {
269+
return;
270+
}
271+
if (response.entityWrappedKey.length === 0) {
272+
return;
273+
}
274+
275+
response.responses = [
276+
create(PolicyRewrapResultSchema, {
277+
policyId: 'policy',
278+
results: [
279+
create(KeyAccessRewrapResultSchema, {
280+
keyAccessObjectId: 'kao-0',
281+
status: 'permit',
282+
result: {
283+
case: 'kasWrappedKey',
284+
value: response.entityWrappedKey,
285+
},
286+
}),
287+
],
288+
}),
289+
];
290+
}

0 commit comments

Comments
 (0)