@virtru/dsp-sdk 0.5.0 → 0.6.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +8 -1
  2. package/README.md +81 -3
  3. package/dist/src/fips.d.ts +7 -0
  4. package/dist/src/fips.js +12 -0
  5. package/dist/src/lib/dsp.d.ts +17 -1
  6. package/dist/src/lib/dsp.js +28 -5
  7. package/package.json +23 -2
  8. package/.prettierrc +0 -3
  9. package/.rush/temp/chunked-rush-logs/dsp-sdk._phase_build.chunks.jsonl +0 -12
  10. package/.rush/temp/chunked-rush-logs/dsp-sdk._phase_test.chunks.jsonl +0 -383
  11. package/.rush/temp/package-deps__phase_build.json +0 -76
  12. package/.rush/temp/shrinkwrap-deps.json +0 -1262
  13. package/CHANGELOG.json +0 -299
  14. package/buf.gen.yaml +0 -17
  15. package/buf.yaml +0 -4
  16. package/config/jest.config.json +0 -6
  17. package/config/rig.json +0 -8
  18. package/dist/tests/dsp-client.test.d.ts +0 -1
  19. package/dist/tests/dsp-client.test.js +0 -73
  20. package/dist/tests/dsp.test.d.ts +0 -1
  21. package/dist/tests/dsp.test.js +0 -92
  22. package/dist/tests/fips-crypto.unit.test.d.ts +0 -1
  23. package/dist/tests/fips-crypto.unit.test.js +0 -258
  24. package/dist/tests/mocks/create-export-artifacts.d.ts +0 -2
  25. package/dist/tests/mocks/create-export-artifacts.js +0 -13
  26. package/dist/tests/mocks/tagging-pdp-tag.d.ts +0 -2
  27. package/dist/tests/mocks/tagging-pdp-tag.js +0 -11
  28. package/dist/tests/mocks/well-known-configuration.d.ts +0 -2
  29. package/dist/tests/mocks/well-known-configuration.js +0 -12
  30. package/dist/tests/setup-msw.d.ts +0 -9
  31. package/dist/tests/setup-msw.js +0 -30
  32. package/dist/tests/setup-unit.d.ts +0 -7
  33. package/dist/tests/setup-unit.js +0 -70
  34. package/eslint.config.mjs +0 -28
  35. package/lib-commonjs/src/gen/buf/validate/validate_pb.js +0 -487
  36. package/lib-commonjs/src/gen/config/v1/config_pb.js +0 -142
  37. package/lib-commonjs/src/gen/config/v1/kas_config_pb.js +0 -72
  38. package/lib-commonjs/src/gen/config/v1/meta_pb.js +0 -36
  39. package/lib-commonjs/src/gen/config/v1/outlook_config_pb.js +0 -67
  40. package/lib-commonjs/src/gen/config/v1/secureviewer_config_pb.js +0 -67
  41. package/lib-commonjs/src/gen/config/v1/tagging_pb.js +0 -246
  42. package/lib-commonjs/src/gen/google/api/annotations_pb.js +0 -33
  43. package/lib-commonjs/src/gen/google/api/http_pb.js +0 -44
  44. package/lib-commonjs/src/gen/policyimportexport/v1/policy_import_export_pb.js +0 -93
  45. package/lib-commonjs/src/gen/shared/v1/shared_pb.js +0 -95
  46. package/lib-commonjs/src/gen/tagging/pdp/v2/tagging_pb.js +0 -277
  47. package/lib-commonjs/src/gen/version/v1/version_pb.js +0 -40
  48. package/lib-commonjs/src/gen/virtru/common/common_pb.js +0 -76
  49. package/lib-commonjs/src/gen/virtru/policy/certificates/v1/certificates_pb.js +0 -77
  50. package/lib-commonjs/src/gen/virtru/policy/objects_pb.js +0 -143
  51. package/lib-commonjs/src/index.js +0 -48
  52. package/lib-commonjs/src/lib/client.js +0 -66
  53. package/lib-commonjs/src/lib/consts.js +0 -10
  54. package/lib-commonjs/src/lib/dsp.js +0 -49
  55. package/lib-commonjs/src/lib/fips-crypto/asymmetric.js +0 -126
  56. package/lib-commonjs/src/lib/fips-crypto/key-format.js +0 -243
  57. package/lib-commonjs/src/lib/fips-crypto/keys.js +0 -134
  58. package/lib-commonjs/src/lib/fips-crypto/primitives.js +0 -112
  59. package/lib-commonjs/src/lib/fips-crypto/symmetric.js +0 -162
  60. package/lib-commonjs/src/lib/fips-crypto-service.js +0 -57
  61. package/lib-commonjs/src/lib/utils.js +0 -401
  62. package/lib-commonjs/tests/dsp-client.test.js +0 -75
  63. package/lib-commonjs/tests/dsp.test.js +0 -94
  64. package/lib-commonjs/tests/fips-crypto.unit.test.js +0 -260
  65. package/lib-commonjs/tests/mocks/create-export-artifacts.js +0 -17
  66. package/lib-commonjs/tests/mocks/tagging-pdp-tag.js +0 -15
  67. package/lib-commonjs/tests/mocks/well-known-configuration.js +0 -16
  68. package/lib-commonjs/tests/setup-msw.js +0 -33
  69. package/lib-commonjs/tests/setup-unit.js +0 -72
  70. package/public/virtru.wasm +0 -0
  71. package/src/gen/buf/validate/validate_pb.ts +0 -4879
  72. package/src/gen/config/formatters/testdata/v1/test_pb.ts +0 -287
  73. package/src/gen/config/v1/config_connect.ts +0 -111
  74. package/src/gen/config/v1/config_pb.ts +0 -439
  75. package/src/gen/config/v1/kas_config_pb.ts +0 -183
  76. package/src/gen/config/v1/meta_pb.ts +0 -72
  77. package/src/gen/config/v1/outlook_config_pb.ts +0 -242
  78. package/src/gen/config/v1/secureviewer_config_pb.ts +0 -161
  79. package/src/gen/config/v1/tagging_pb.ts +0 -456
  80. package/src/gen/google/api/annotations_pb.ts +0 -43
  81. package/src/gen/google/api/http_pb.ts +0 -486
  82. package/src/gen/helloworld/v1/helloworld_connect.ts +0 -49
  83. package/src/gen/helloworld/v1/helloworld_pb.ts +0 -104
  84. package/src/gen/kas/nanotdf/v1/nanotdf_rewrap_connect.ts +0 -39
  85. package/src/gen/kas/nanotdf/v1/nanotdf_rewrap_pb.ts +0 -207
  86. package/src/gen/policyimportexport/v1/policy_import_export_connect.ts +0 -56
  87. package/src/gen/policyimportexport/v1/policy_import_export_pb.ts +0 -332
  88. package/src/gen/shared/v1/shared_connect.ts +0 -61
  89. package/src/gen/shared/v1/shared_pb.ts +0 -240
  90. package/src/gen/shared/v2/shared_connect.ts +0 -39
  91. package/src/gen/shared/v2/shared_pb.ts +0 -122
  92. package/src/gen/tagging/pdp/v2/externalprocessors/processor_connect.ts +0 -156
  93. package/src/gen/tagging/pdp/v2/externalprocessors/processor_pb.ts +0 -709
  94. package/src/gen/tagging/pdp/v2/stanag/stanag_pb.ts +0 -953
  95. package/src/gen/tagging/pdp/v2/tagging_connect.ts +0 -73
  96. package/src/gen/tagging/pdp/v2/tagging_pb.ts +0 -1064
  97. package/src/gen/version/v1/version_connect.ts +0 -31
  98. package/src/gen/version/v1/version_pb.ts +0 -143
  99. package/src/gen/virtru/common/common_pb.ts +0 -201
  100. package/src/gen/virtru/policy/certificates/v1/certificates_connect.ts +0 -44
  101. package/src/gen/virtru/policy/certificates/v1/certificates_pb.ts +0 -305
  102. package/src/gen/virtru/policy/objects_pb.ts +0 -253
  103. package/src/gen/web-admin/v1/config_connect.ts +0 -52
  104. package/src/gen/web-admin/v1/config_pb.ts +0 -173
  105. package/src/index.ts +0 -62
  106. package/src/lib/client.ts +0 -90
  107. package/src/lib/consts.ts +0 -8
  108. package/src/lib/dsp.ts +0 -65
  109. package/src/lib/fips-crypto/asymmetric.ts +0 -162
  110. package/src/lib/fips-crypto/key-format.ts +0 -287
  111. package/src/lib/fips-crypto/keys.ts +0 -190
  112. package/src/lib/fips-crypto/primitives.ts +0 -197
  113. package/src/lib/fips-crypto/symmetric.ts +0 -213
  114. package/src/lib/fips-crypto-service.ts +0 -58
  115. package/src/lib/utils.test.ts +0 -415
  116. package/src/lib/utils.ts +0 -525
  117. package/src/vite-env.d.ts +0 -7
  118. package/temp/build/lint/_eslint-5eVG3S6w.json +0 -38
  119. package/temp/test/jest/haste-map-b055a9550e6d9f9b43d8ad29b1607278-49d19aee56a0732eca9c014a51272338-8710993d07b75a801b0956e67ffc46fa +0 -0
  120. package/tests/dsp-client.test.ts +0 -95
  121. package/tests/dsp.test.ts +0 -119
  122. package/tests/fips-crypto.unit.test.ts +0 -412
  123. package/tests/mocks/create-export-artifacts.ts +0 -16
  124. package/tests/mocks/tagging-pdp-tag.ts +0 -13
  125. package/tests/mocks/well-known-configuration.ts +0 -15
  126. package/tests/setup-msw.ts +0 -35
  127. package/tests/setup-unit.ts +0 -108
  128. package/tsconfig.json +0 -32
  129. package/update-protos.sh +0 -63
  130. package/vitest.config.ts +0 -48
@@ -1,213 +0,0 @@
1
- /*
2
- * Copyright (c) Virtru Corporation. All rights reserved.
3
- *
4
- * Your use of this file is governed by the Virtu Terms of Service <https://www.virtru.com/terms-of-service/>.
5
- */
6
-
7
- import { Binary, type SymmetricKey } from '@opentdf/sdk/singlecontainer';
8
- import { ConfigurationError } from '@opentdf/sdk';
9
- import { getVirtruCrypto } from './primitives';
10
- import { isSymmetricKey, wrapSymmetricKey, unwrapSymmetricKey } from './keys';
11
-
12
- type AlgorithmUrn = string;
13
- type EncryptResult = { payload: Binary; authTag?: Binary };
14
- type DecryptResult = { payload: Binary };
15
-
16
- const AES_CBC_URN = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc';
17
-
18
- const SYM_KEY_METHODS = ['encrypt', 'decrypt', 'sign', 'verify'] as const;
19
-
20
- /**
21
- * Generate a random AES-256 symmetric key.
22
- * The VirtruCryptoKey handle is kept alive inside the opaque SymmetricKey —
23
- * key material never leaves the FIPS WASM keystore.
24
- */
25
- export async function generateKey(length?: number): Promise<SymmetricKey> {
26
- const crypto = await getVirtruCrypto();
27
- const bitLen = (length || 32) * 8;
28
- // extractable=true and internal=true: keystore-based AES key that can be exported
29
- // for RSA key-wrapping (encryptWithPublicKey calls exportKey on the DEK).
30
- const vKey = await crypto.generateKey(
31
- { name: 'AES-GCM', length: bitLen as 256 },
32
- true,
33
- SYM_KEY_METHODS,
34
- true
35
- );
36
- return wrapSymmetricKey(vKey, bitLen);
37
- }
38
-
39
- /**
40
- * Encrypt a payload with a symmetric AES key.
41
- * When payload is a SymmetricKey the key bytes are exported internally to
42
- * perform key-wrapping — they are never surfaced to the caller.
43
- */
44
- export async function encrypt(
45
- payload: Binary | SymmetricKey,
46
- key: SymmetricKey,
47
- iv: Binary,
48
- algorithm?: AlgorithmUrn
49
- ): Promise<EncryptResult> {
50
- const crypto = await getVirtruCrypto();
51
- const algName = algorithm === AES_CBC_URN ? 'AES-CBC' : 'AES-GCM';
52
-
53
- let payloadBytes: Uint8Array;
54
- if (isSymmetricKey(payload)) {
55
- // Key-wrapping path: export symmetric key bytes from keystore.
56
- // Only works if the key was created with extractable:true (e.g. via importSymmetricKey).
57
- payloadBytes = await crypto.exportKey('raw', unwrapSymmetricKey(payload));
58
- } else {
59
- payloadBytes = new Uint8Array(payload.asArrayBuffer());
60
- }
61
-
62
- const vKey = unwrapSymmetricKey(key);
63
- const ivBytes = new Uint8Array(iv.asArrayBuffer());
64
-
65
- const cipherBytes = await crypto.encrypt(
66
- { name: algName, length: 256, iv: ivBytes } as any,
67
- vKey,
68
- payloadBytes
69
- );
70
-
71
- if (algName === 'AES-GCM') {
72
- return {
73
- payload: Binary.fromArrayBuffer(cipherBytes.slice(0, -16).buffer as ArrayBuffer),
74
- authTag: Binary.fromArrayBuffer(cipherBytes.slice(-16).buffer as ArrayBuffer),
75
- };
76
- }
77
- return { payload: Binary.fromArrayBuffer(cipherBytes.buffer as ArrayBuffer) };
78
- }
79
-
80
- /**
81
- * Decrypt a payload with a symmetric AES key.
82
- */
83
- export async function decrypt(
84
- payload: Binary,
85
- key: SymmetricKey,
86
- iv: Binary,
87
- algorithm?: AlgorithmUrn,
88
- authTag?: Binary
89
- ): Promise<DecryptResult> {
90
- const crypto = await getVirtruCrypto();
91
- const algName = algorithm === AES_CBC_URN ? 'AES-CBC' : 'AES-GCM';
92
-
93
- let payloadBytes = new Uint8Array(payload.asArrayBuffer());
94
- const ivBytes = new Uint8Array(iv.asArrayBuffer());
95
-
96
- if (algName === 'AES-GCM') {
97
- if (!authTag) {
98
- throw new Error('Missing authTag for AES-GCM decryption');
99
- }
100
- const tagBytes = new Uint8Array(authTag.asArrayBuffer());
101
- const combined = new Uint8Array(payloadBytes.length + tagBytes.length);
102
- combined.set(payloadBytes, 0);
103
- combined.set(tagBytes, payloadBytes.length);
104
- payloadBytes = combined;
105
- }
106
-
107
- const vKey = unwrapSymmetricKey(key);
108
- const plainBytes = await crypto.decrypt(
109
- { name: algName, length: 256, iv: ivBytes } as any,
110
- vKey,
111
- payloadBytes
112
- );
113
-
114
- return { payload: Binary.fromArrayBuffer(plainBytes.buffer as ArrayBuffer) };
115
- }
116
-
117
- // ─── HMAC ─────────────────────────────────────────────────────────────────────
118
-
119
- const HMAC_PARAMS = { name: 'HMAC' as const, hash: 'SHA-256' as const };
120
-
121
- /**
122
- * Compute HMAC-SHA256.
123
- * The SymmetricKey must have been created with extractable:true (e.g. via
124
- * importSymmetricKey) so its bytes can be re-imported as an HMAC key.
125
- */
126
- export async function hmac(data: Uint8Array, key: SymmetricKey): Promise<Uint8Array> {
127
- const crypto = await getVirtruCrypto();
128
- const vKey = unwrapSymmetricKey(key);
129
- // The key was imported with 'sign' usage (see importSymmetricKey).
130
- // VirtruCrypto sign() checks usage.includes('sign') then calls the WASM
131
- // hmac() with the slot number — the key type string is irrelevant for HMAC.
132
- // Do NOT call release() here: the key handle must remain valid for future calls.
133
- const sig = await crypto.sign(HMAC_PARAMS, vKey, data);
134
- return new Uint8Array(sig);
135
- }
136
-
137
- /**
138
- * Verify HMAC-SHA256 using constant-time comparison.
139
- */
140
- export async function verifyHmac(
141
- data: Uint8Array,
142
- signature: Uint8Array,
143
- key: SymmetricKey
144
- ): Promise<boolean> {
145
- const expected = await hmac(data, key);
146
- if (signature.length !== expected.length) return false;
147
- let mismatch = 0;
148
- for (let i = 0; i < signature.length; i++) {
149
- mismatch |= signature[i] ^ expected[i];
150
- }
151
- return mismatch === 0;
152
- }
153
-
154
- // ─── Symmetric Key Import / Split / Merge ─────────────────────────────────────
155
-
156
- /**
157
- * Import raw key bytes as an opaque SymmetricKey.
158
- * The resulting key is extractable so it can be used for HMAC and key-wrapping.
159
- */
160
- export async function importSymmetricKey(keyBytes: Uint8Array): Promise<SymmetricKey> {
161
- const crypto = await getVirtruCrypto();
162
- const bitLen = keyBytes.length * 8;
163
- // Include 'sign'/'verify' so this key can also be used for HMAC-SHA256.
164
- // VirtruCrypto's sign() checks key.usage.includes('sign'); the underlying
165
- // WASM hmac() takes the slot number and works with any 256-bit key material.
166
- const vKey = await crypto.importKey(
167
- 'raw',
168
- keyBytes,
169
- { name: 'AES-GCM' },
170
- true,
171
- SYM_KEY_METHODS
172
- );
173
- return wrapSymmetricKey(vKey, bitLen);
174
- }
175
-
176
- /**
177
- * Split a symmetric key into N shares using XOR secret sharing.
178
- *
179
- * For numShares === 1 (single-KAS), the key is returned as-is — no key
180
- * material is exported and no splitting occurs. This is the common DSP case.
181
- *
182
- * For numShares > 1 (multi-KAS), XOR secret sharing requires exporting the
183
- * raw key bytes. This is not yet implemented in FIPS mode.
184
- */
185
- export async function splitSymmetricKey(key: SymmetricKey, numShares: number): Promise<SymmetricKey[]> {
186
- if (numShares === 1) {
187
- return [key];
188
- }
189
- throw new ConfigurationError(
190
- `splitSymmetricKey with ${numShares} shares is not yet supported in FIPS mode. ` +
191
- 'Multi-KAS key splitting requires raw key export which violates FIPS key boundary constraints.'
192
- );
193
- }
194
-
195
- /**
196
- * Merge symmetric key shares back into the original key using XOR.
197
- *
198
- * For a single share (single-KAS), the share is returned as-is — no key
199
- * material is exported and no XOR occurs. This is the common DSP case.
200
- *
201
- * For multiple shares (multi-KAS), XOR merging requires exporting raw key bytes.
202
- * This is not yet implemented in FIPS mode.
203
- */
204
- export async function mergeSymmetricKeys(shares: SymmetricKey[]): Promise<SymmetricKey> {
205
- if (shares.length === 0) throw new ConfigurationError('No shares provided to mergeSymmetricKeys');
206
- if (shares.length === 1) {
207
- return shares[0];
208
- }
209
- throw new ConfigurationError(
210
- `mergeSymmetricKeys with ${shares.length} shares is not yet supported in FIPS mode. ` +
211
- 'Multi-KAS key merging requires raw key export which violates FIPS key boundary constraints.'
212
- );
213
- }
@@ -1,58 +0,0 @@
1
- /*
2
- * Copyright (c) Virtru Corporation. All rights reserved.
3
- *
4
- * Your use of this file is governed by the Virtu Terms of Service <https://www.virtru.com/terms-of-service/>.
5
- */
6
-
7
- import { type CryptoService } from '@opentdf/sdk/singlecontainer';
8
-
9
- export { initializeFipsCrypto } from './fips-crypto/primitives';
10
-
11
- export {
12
- wrapPublicKey,
13
- wrapPrivateKey,
14
- wrapSymmetricKey,
15
- wrapKeyPair,
16
- unwrapPublicKey,
17
- unwrapPrivateKey,
18
- unwrapSymmetricKey,
19
- } from './fips-crypto/keys';
20
-
21
- import { randomBytes, digest } from './fips-crypto/primitives';
22
- import { generateKey, encrypt, decrypt, hmac, verifyHmac, importSymmetricKey, splitSymmetricKey, mergeSymmetricKeys } from './fips-crypto/symmetric';
23
- import { generateKeyPair, generateSigningKeyPair, encryptWithPublicKey, decryptWithPrivateKey, sign, verify, generateECKeyPair, deriveKeyFromECDH } from './fips-crypto/asymmetric';
24
- import { importPublicKey, importPrivateKey, exportPublicKeyPem, exportPrivateKeyPem, exportPublicKeyJwk, parsePublicKeyPem, extractPublicKeyPem, jwkToPublicKeyPem } from './fips-crypto/key-format';
25
- export { importPrivateKey, exportPrivateKeyPem } from './fips-crypto/key-format';
26
-
27
- // ─── FipsCryptoService ────────────────────────────────────────────────────────
28
-
29
- export const FipsCryptoService: CryptoService = {
30
- name: 'FipsCryptoService',
31
- method: 'http://www.w3.org/2001/04/xmlenc#aes256-cbc',
32
- decrypt,
33
- decryptWithPrivateKey,
34
- encrypt,
35
- encryptWithPublicKey,
36
- generateKey,
37
- generateKeyPair,
38
- generateSigningKeyPair,
39
- randomBytes,
40
- sign,
41
- verify,
42
- hmac,
43
- verifyHmac,
44
- digest,
45
- generateECKeyPair,
46
- deriveKeyFromECDH,
47
- importPublicKey,
48
- importPrivateKey,
49
- exportPublicKeyPem,
50
- exportPrivateKeyPem,
51
- exportPublicKeyJwk,
52
- parsePublicKeyPem,
53
- extractPublicKeyPem,
54
- jwkToPublicKeyPem,
55
- importSymmetricKey,
56
- splitSymmetricKey,
57
- mergeSymmetricKeys,
58
- };
@@ -1,415 +0,0 @@
1
- /*
2
- * Copyright (c) Virtru Corporation. All rights reserved.
3
- *
4
- * Your use of this file is governed by the Virtu Terms of Service <https://www.virtru.com/terms-of-service/>.
5
- */
6
-
7
- import { describe, it, expect, beforeEach, vi } from 'vitest';
8
-
9
- // Mock all @opentdf imports BEFORE importing utils
10
- vi.mock('@opentdf/sdk', () => ({
11
- AuthProviders: {
12
- refreshAuthProvider: vi.fn(),
13
- },
14
- OpenTDF: vi.fn(),
15
- PermissionDeniedError: class PermissionDeniedError extends Error {},
16
- }));
17
-
18
- vi.mock('@opentdf/sdk/singlecontainer', () => ({
19
- WebCryptoService: {
20
- generateSigningKeyPair: vi.fn().mockResolvedValue({
21
- publicKey: {},
22
- privateKey: {},
23
- }),
24
- },
25
- }));
26
-
27
- vi.mock('@opentdf/sdk/platform', () => ({
28
- PlatformClient: vi.fn(),
29
- }));
30
-
31
- vi.mock('@opentdf/sdk/platform/policy/objects_pb.js', () => ({
32
- Certificate: class Certificate {
33
- pem?: string;
34
- constructor(data?: any) {
35
- if (data) Object.assign(this, data);
36
- }
37
- },
38
- }));
39
-
40
- vi.mock('@opentdf/sdk/platform/common/common_pb.js', () => ({
41
- ActiveStateEnum: {
42
- ACTIVE: 1,
43
- },
44
- }));
45
-
46
- // Polyfill atob if needed
47
- if (typeof (globalThis as any).atob === 'undefined') {
48
- (globalThis as any).atob = (b64: string) =>
49
- Buffer.from(b64, 'base64').toString('binary');
50
- }
51
-
52
- // Mock pkijs/asn1js with a way to control them
53
- const mockState = {
54
- asn1Mode: 'ok' as 'ok' | 'bad-der',
55
- verifyResult: { result: true } as { result: boolean; resultMessage?: string },
56
- };
57
-
58
- vi.mock('asn1js', () => ({
59
- fromBER: vi.fn((_: ArrayBuffer) => {
60
- if (mockState.asn1Mode === 'bad-der') return { offset: -1 };
61
- return {
62
- offset: 0,
63
- result: {
64
- subject: { typesAndValues: [] },
65
- issuer: { typesAndValues: [] },
66
- },
67
- };
68
- }),
69
- }));
70
-
71
- vi.mock('pkijs', () => {
72
- return {
73
- Certificate: class Certificate {
74
- public subject: any = { typesAndValues: [] };
75
- public issuer: any = { typesAndValues: [] };
76
- public subjectPublicKeyInfo: any = {};
77
- constructor(_: any) {}
78
- },
79
- CertificateChainValidationEngine: class CertificateChainValidationEngine {
80
- constructor(_: any) {}
81
- async verify() {
82
- return mockState.verifyResult;
83
- }
84
- },
85
- };
86
- });
87
-
88
- // Mock DSPClient
89
- vi.mock('./client', () => ({
90
- DSPClient: vi.fn(),
91
- }));
92
-
93
- // NOW import utils
94
- import { validateJWSCertificateChain, getTrustedCertificates } from './utils';
95
- import { DSPClient } from './client';
96
- import { PlatformClient } from '@opentdf/sdk/platform';
97
- import { AuthProviders } from '@opentdf/sdk';
98
- import { WebCryptoService } from '@opentdf/sdk/singlecontainer';
99
-
100
- describe('validateJWSCertificateChain', () => {
101
- beforeEach(() => {
102
- mockState.asn1Mode = 'ok';
103
- mockState.verifyResult = { result: true };
104
- });
105
-
106
- it('throws error when x5c is empty', async () => {
107
- await expect(
108
- validateJWSCertificateChain([], [{ pem: '---' }] as any)
109
- ).rejects.toThrow(/No certificates provided in x5c array/);
110
- });
111
-
112
- it('throws error when too many certificates in x5c', async () => {
113
- const tooMany = new Array(11).fill('AQI=');
114
- await expect(
115
- validateJWSCertificateChain(tooMany, [{ pem: '---' }] as any)
116
- ).rejects.toThrow(/Too many certificates in x5c/);
117
- });
118
-
119
- it('throws error when no trusted roots provided', async () => {
120
- await expect(
121
- validateJWSCertificateChain(['AQI='], [] as any)
122
- ).rejects.toThrow(/No trusted root certificates provided/);
123
- });
124
-
125
- it('fails on invalid base64 in x5c', async () => {
126
- await expect(
127
- validateJWSCertificateChain(['***not-base64***'], [{ pem: '---' }] as any)
128
- ).rejects.toThrow(/Failed to parse certificate at x5c\[0]/);
129
- });
130
-
131
- it('fails on ASN.1 parse error', async () => {
132
- mockState.asn1Mode = 'bad-der';
133
- await expect(
134
- validateJWSCertificateChain(['AQI='], [{ pem: '---' }] as any)
135
- ).rejects.toThrow(/Invalid DER encoding/);
136
- });
137
-
138
- it('returns valid true on successful verification', async () => {
139
- const pem = '-----BEGIN CERTIFICATE-----\nAQI=\n-----END CERTIFICATE-----';
140
- mockState.verifyResult = { result: true };
141
- const res = await validateJWSCertificateChain(['AQI='], [{ pem }] as any);
142
- expect(res.valid).toBe(true);
143
- expect(res.validationResult.warnings).toEqual([]);
144
- });
145
-
146
- it('returns warnings when some trusted roots fail to parse', async () => {
147
- const pem = '-----BEGIN CERTIFICATE-----\nAQI=\n-----END CERTIFICATE-----';
148
- const res = await validateJWSCertificateChain(['AQI='], [
149
- { pem: '' },
150
- { pem },
151
- ] as any);
152
- expect(res.valid).toBe(true);
153
- expect(res.validationResult.warnings).toHaveLength(1);
154
- expect(res.validationResult.warnings[0]).toMatch(/has no PEM data/);
155
- });
156
- });
157
-
158
- describe('getTrustedCertificates', () => {
159
- const mockOptions = {
160
- oidc: {
161
- clientId: 'test-client-id',
162
- tokenEndpoint: 'https://auth.test.com/token',
163
- userInfoEndpoint: 'https://auth.test.com/userinfo',
164
- },
165
- platformEndpoint: 'https://platform.test.com',
166
- refreshToken: 'test-refresh-token',
167
- };
168
-
169
- let mockAuthProvider: any;
170
- let mockPlatformClient: any;
171
- let mockDSPClient: any;
172
- let mockCertificateService: any;
173
-
174
- beforeEach(() => {
175
- vi.clearAllMocks();
176
-
177
- // Mock crypto.subtle.generateKey for DPoP signing keys
178
- if (!globalThis.crypto) {
179
- (globalThis as any).crypto = {};
180
- }
181
- if (!globalThis.crypto.subtle) {
182
- (globalThis.crypto as any).subtle = {};
183
- }
184
- (globalThis.crypto.subtle as any).generateKey = vi.fn().mockResolvedValue({
185
- publicKey: {},
186
- privateKey: {},
187
- });
188
-
189
- // Mock auth provider
190
- mockAuthProvider = {
191
- updateClientPublicKey: vi.fn().mockResolvedValue(undefined),
192
- };
193
-
194
- (AuthProviders.refreshAuthProvider as any).mockResolvedValue(
195
- mockAuthProvider
196
- );
197
-
198
- // Mock certificate service
199
- mockCertificateService = {
200
- listCertificatesByNamespace: vi.fn(),
201
- };
202
-
203
- // Mock DSP client
204
- mockDSPClient = {
205
- v1: {
206
- certificateService: mockCertificateService,
207
- },
208
- };
209
-
210
- (DSPClient as any).mockImplementation(() => mockDSPClient);
211
-
212
- // Mock platform client
213
- mockPlatformClient = {
214
- v1: {
215
- namespace: {
216
- listNamespaces: vi.fn(),
217
- },
218
- },
219
- };
220
-
221
- (PlatformClient as any).mockImplementation(() => mockPlatformClient);
222
- });
223
-
224
- it('successfully retrieves certificates from a single namespace', async () => {
225
- const mockCertificate1 = { pem: 'cert1-pem-data' };
226
- const mockCertificate2 = { pem: 'cert2-pem-data' };
227
-
228
- mockPlatformClient.v1.namespace.listNamespaces.mockResolvedValue({
229
- namespaces: [{ id: 'namespace-1' }],
230
- });
231
-
232
- mockCertificateService.listCertificatesByNamespace.mockResolvedValue({
233
- certificates: [mockCertificate1, mockCertificate2],
234
- pagination: { nextOffset: 0 },
235
- });
236
-
237
- const result = await getTrustedCertificates(mockOptions);
238
-
239
- expect(result).toHaveLength(2);
240
- expect(result).toEqual([mockCertificate1, mockCertificate2]);
241
- expect(
242
- mockCertificateService.listCertificatesByNamespace
243
- ).toHaveBeenCalledWith({
244
- namespaceId: 'namespace-1',
245
- pagination: { limit: 100, offset: 0 },
246
- });
247
- });
248
-
249
- it('successfully retrieves certificates from multiple namespaces', async () => {
250
- const mockCert1 = { pem: 'cert1-pem' };
251
- const mockCert2 = { pem: 'cert2-pem' };
252
- const mockCert3 = { pem: 'cert3-pem' };
253
-
254
- mockPlatformClient.v1.namespace.listNamespaces.mockResolvedValue({
255
- namespaces: [{ id: 'namespace-1' }, { id: 'namespace-2' }],
256
- });
257
-
258
- mockCertificateService.listCertificatesByNamespace
259
- .mockResolvedValueOnce({
260
- certificates: [mockCert1, mockCert2],
261
- pagination: { nextOffset: 0 },
262
- })
263
- .mockResolvedValueOnce({
264
- certificates: [mockCert3],
265
- pagination: { nextOffset: 0 },
266
- });
267
-
268
- const result = await getTrustedCertificates(mockOptions);
269
-
270
- expect(result).toHaveLength(3);
271
- expect(result).toEqual([mockCert1, mockCert2, mockCert3]);
272
- expect(
273
- mockCertificateService.listCertificatesByNamespace
274
- ).toHaveBeenCalledTimes(2);
275
- });
276
-
277
- it('handles pagination correctly (multiple pages)', async () => {
278
- const mockCert1 = { pem: 'cert1' };
279
- const mockCert2 = { pem: 'cert2' };
280
- const mockCert3 = { pem: 'cert3' };
281
-
282
- mockPlatformClient.v1.namespace.listNamespaces.mockResolvedValue({
283
- namespaces: [{ id: 'namespace-1' }],
284
- });
285
-
286
- // First page
287
- mockCertificateService.listCertificatesByNamespace
288
- .mockResolvedValueOnce({
289
- certificates: [mockCert1, mockCert2],
290
- pagination: { nextOffset: 100 },
291
- })
292
- // Second page
293
- .mockResolvedValueOnce({
294
- certificates: [mockCert3],
295
- pagination: { nextOffset: 0 },
296
- });
297
-
298
- const result = await getTrustedCertificates(mockOptions);
299
-
300
- expect(result).toHaveLength(3);
301
- expect(result).toEqual([mockCert1, mockCert2, mockCert3]);
302
- expect(
303
- mockCertificateService.listCertificatesByNamespace
304
- ).toHaveBeenCalledTimes(2);
305
- expect(
306
- mockCertificateService.listCertificatesByNamespace
307
- ).toHaveBeenNthCalledWith(1, {
308
- namespaceId: 'namespace-1',
309
- pagination: { limit: 100, offset: 0 },
310
- });
311
- expect(
312
- mockCertificateService.listCertificatesByNamespace
313
- ).toHaveBeenNthCalledWith(2, {
314
- namespaceId: 'namespace-1',
315
- pagination: { limit: 100, offset: 100 },
316
- });
317
- });
318
-
319
- it('handles empty certificate list', async () => {
320
- mockPlatformClient.v1.namespace.listNamespaces.mockResolvedValue({
321
- namespaces: [{ id: 'namespace-1' }],
322
- });
323
-
324
- mockCertificateService.listCertificatesByNamespace.mockResolvedValue({
325
- certificates: [],
326
- pagination: { nextOffset: 0 },
327
- });
328
-
329
- const result = await getTrustedCertificates(mockOptions);
330
-
331
- expect(result).toHaveLength(0);
332
- expect(result).toEqual([]);
333
- });
334
-
335
- it('handles errors from certificate service', async () => {
336
- mockPlatformClient.v1.namespace.listNamespaces.mockResolvedValue({
337
- namespaces: [{ id: 'namespace-1' }],
338
- });
339
-
340
- mockCertificateService.listCertificatesByNamespace.mockRejectedValue(
341
- new Error('Certificate service error')
342
- );
343
-
344
- await expect(getTrustedCertificates(mockOptions)).rejects.toThrow(
345
- 'Certificate service error'
346
- );
347
- });
348
-
349
- it('handles errors from namespace listing', async () => {
350
- mockPlatformClient.v1.namespace.listNamespaces.mockRejectedValue(
351
- new Error('Namespace listing error')
352
- );
353
-
354
- await expect(getTrustedCertificates(mockOptions)).rejects.toThrow(
355
- 'Namespace listing error'
356
- );
357
- });
358
-
359
- it('initializes auth provider correctly', async () => {
360
- mockPlatformClient.v1.namespace.listNamespaces.mockResolvedValue({
361
- namespaces: [],
362
- });
363
-
364
- await getTrustedCertificates(mockOptions);
365
-
366
- expect(AuthProviders.refreshAuthProvider).toHaveBeenCalled();
367
- expect(WebCryptoService.generateSigningKeyPair).toHaveBeenCalled();
368
- expect(mockAuthProvider.updateClientPublicKey).toHaveBeenCalled();
369
- });
370
-
371
- it('creates both PlatformClient and DSPClient with correct config', async () => {
372
- mockPlatformClient.v1.namespace.listNamespaces.mockResolvedValue({
373
- namespaces: [],
374
- });
375
-
376
- await getTrustedCertificates(mockOptions);
377
-
378
- expect(PlatformClient).toHaveBeenCalledWith({
379
- authProvider: mockAuthProvider,
380
- platformUrl: mockOptions.platformEndpoint,
381
- });
382
-
383
- expect(DSPClient).toHaveBeenCalledWith({
384
- platformUrl: mockOptions.platformEndpoint,
385
- authProvider: mockAuthProvider,
386
- });
387
- });
388
-
389
- it('returns same Certificate type compatible with validateJWSCertificateChain', async () => {
390
- const mockCert = {
391
- pem: '-----BEGIN CERTIFICATE-----\nAQI=\n-----END CERTIFICATE-----',
392
- };
393
-
394
- mockPlatformClient.v1.namespace.listNamespaces.mockResolvedValue({
395
- namespaces: [{ id: 'namespace-1' }],
396
- });
397
-
398
- mockCertificateService.listCertificatesByNamespace.mockResolvedValue({
399
- certificates: [mockCert],
400
- pagination: { nextOffset: 0 },
401
- });
402
-
403
- const result = await getTrustedCertificates(mockOptions);
404
-
405
- // Verify the result can be used with validateJWSCertificateChain
406
- expect(result[0].pem).toBeDefined();
407
-
408
- // Integration test: verify compatibility with validateJWSCertificateChain
409
- const validationResult = await validateJWSCertificateChain(
410
- ['AQI='],
411
- result
412
- );
413
- expect(validationResult.valid).toBe(true);
414
- });
415
- });