Skip to content

Commit 242150b

Browse files
feat: Assertion signing key handling and verification in the CLI (#409)
1 parent 1beb02c commit 242150b

File tree

3 files changed

+143
-5
lines changed

3 files changed

+143
-5
lines changed

cli/src/cli.ts

Lines changed: 129 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { webcrypto } from 'crypto';
2222
import * as assertions from '@opentdf/sdk/assertions';
2323
import { attributeFQNsAsValues } from '@opentdf/sdk/nano';
2424
import { base64 } from '@opentdf/sdk/encodings';
25+
import { importPKCS8, importSPKI, KeyLike } from 'jose'; // for RS256
2526

2627
type AuthToProcess = {
2728
auth?: string;
@@ -120,11 +121,71 @@ function addParams(client: AnyNanoClient, argv: Partial<mainArgs>) {
120121
log('SILLY', `Built encrypt params dissems: ${client.dissems}, attrs: ${client.dataAttributes}`);
121122
}
122123

124+
async function parseAssertionVerificationKeys(
125+
s: string
126+
): Promise<assertions.AssertionVerificationKeys> {
127+
let u;
128+
try {
129+
u = JSON.parse(s);
130+
} catch (err) {
131+
// try as file name:
132+
try {
133+
const jsonFile = await openAsBlob(s);
134+
u = JSON.parse(await jsonFile.text());
135+
} catch (err2) {
136+
throw new CLIError(
137+
'CRITICAL',
138+
`Failed to open/parse assertion verification keys as string or file path ${err.message}`,
139+
err
140+
);
141+
}
142+
}
143+
if (typeof u !== 'object' || u === null) {
144+
throw new Error('Invalid input: The input must be an object');
145+
}
146+
// handle both cases of "keys"
147+
if (!('Keys' in u && typeof u.Keys === 'object')) {
148+
if ('keys' in u && typeof u.keys === 'object') {
149+
u.Keys = u.keys;
150+
} else {
151+
throw new CLIError(
152+
'CRITICAL',
153+
'Invalid input: invalid structure of assertionVerificationKeys'
154+
);
155+
}
156+
}
157+
for (const assertionName in u.Keys) {
158+
const assertionKey = u.Keys[assertionName];
159+
// Ensure each entry has the required 'key' and 'alg' fields
160+
if (typeof assertionKey !== 'object' || assertionKey === null) {
161+
throw new CLIError('CRITICAL', `Invalid assertion for ${assertionName}: Must be an object`);
162+
}
163+
164+
if (typeof assertionKey.key !== 'string' || typeof assertionKey.alg !== 'string') {
165+
throw new CLIError(
166+
'CRITICAL',
167+
`Invalid assertion for ${assertionName}: Missing or invalid 'key' or 'alg'`
168+
);
169+
}
170+
try {
171+
u.Keys[assertionName].key = await correctAssertionKeys(assertionKey.alg, assertionKey.key);
172+
} catch (err) {
173+
throw new CLIError('CRITICAL', `Issue converting assertion key from string: ${err.message}`);
174+
}
175+
}
176+
return u;
177+
}
178+
123179
async function tdf3DecryptParamsFor(argv: Partial<mainArgs>): Promise<DecryptParams> {
124180
const c = new DecryptParamsBuilder();
125181
if (argv.noVerifyAssertions) {
126182
c.withNoVerifyAssertions(true);
127183
}
184+
if (argv.assertionVerificationKeys) {
185+
c.withAssertionVerificationKeys(
186+
await parseAssertionVerificationKeys(argv.assertionVerificationKeys)
187+
);
188+
}
128189
if (argv.concurrencyLimit) {
129190
c.withConcurrencyLimit(argv.concurrencyLimit);
130191
} else {
@@ -134,8 +195,52 @@ async function tdf3DecryptParamsFor(argv: Partial<mainArgs>): Promise<DecryptPar
134195
return c.build();
135196
}
136197

137-
function parseAssertionConfig(s: string): assertions.AssertionConfig[] {
138-
const u = JSON.parse(s);
198+
async function correctAssertionKeys(
199+
alg: string,
200+
key: KeyLike | Uint8Array
201+
): Promise<KeyLike | Uint8Array> {
202+
if (alg === 'HS256') {
203+
// Convert key string to Uint8Array
204+
if (typeof key !== 'string') {
205+
throw new CLIError('CRITICAL', 'HS256 key must be a string');
206+
}
207+
return new TextEncoder().encode(key); // Update array element directly
208+
} else if (alg === 'RS256') {
209+
// Convert PEM string to a KeyLike object
210+
if (typeof key !== 'string') {
211+
throw new CLIError('CRITICAL', 'RS256 key must be a PEM string');
212+
}
213+
try {
214+
return await importPKCS8(key, 'RS256'); // Import private key
215+
} catch (err) {
216+
// If importing as a private key fails, try importing as a public key
217+
try {
218+
return await importSPKI(key, 'RS256'); // Import public key
219+
} catch (err) {}
220+
}
221+
}
222+
// Otherwise its an unsupported alg
223+
throw new CLIError('CRITICAL', `Unsupported signing key algorithm: ${alg}`); // Handle unsupported algs
224+
}
225+
226+
async function parseAssertionConfig(s: string): Promise<assertions.AssertionConfig[]> {
227+
let u;
228+
try {
229+
u = JSON.parse(s);
230+
} catch (err) {
231+
// try as file name:
232+
try {
233+
const jsonFile = await openAsBlob(s);
234+
u = JSON.parse(await jsonFile.text());
235+
} catch (err2) {
236+
throw new CLIError(
237+
'CRITICAL',
238+
`Failed to open/parse assertions as string or file path ${err.message}`,
239+
err
240+
);
241+
}
242+
}
243+
139244
// if u is null or empty, return an empty array
140245
if (!u) {
141246
return [];
@@ -145,14 +250,26 @@ function parseAssertionConfig(s: string): assertions.AssertionConfig[] {
145250
if (!assertions.isAssertionConfig(assertion)) {
146251
throw new CLIError('CRITICAL', `invalid assertion config ${JSON.stringify(assertion)}`);
147252
}
253+
if (assertion.signingKey) {
254+
const { alg, key } = assertion.signingKey;
255+
try {
256+
assertion.signingKey.key = await correctAssertionKeys(alg, key);
257+
} catch (err) {
258+
throw new CLIError(
259+
'CRITICAL',
260+
`Issue converting assertion key from string: ${err.message}`,
261+
err
262+
);
263+
}
264+
}
148265
}
149266
return a;
150267
}
151268

152269
async function tdf3EncryptParamsFor(argv: Partial<mainArgs>): Promise<EncryptParams> {
153270
const c = new EncryptParamsBuilder();
154271
if (argv.assertions?.length) {
155-
c.withAssertions(parseAssertionConfig(argv.assertions));
272+
c.withAssertions(await parseAssertionConfig(argv.assertions));
156273
}
157274
if (argv.attributes?.length) {
158275
c.setAttributes(argv.attributes.split(','));
@@ -249,6 +366,14 @@ export const handleArgs = (args: string[]) => {
249366
desc: 'Do not verify assertions',
250367
type: 'boolean',
251368
})
369+
.option('assertionVerificationKeys', {
370+
alias: 'with-assertion-verification-keys',
371+
group: 'Decrypt',
372+
desc: 'keys for assertion verification or path to a json file containing keys for assertion verification',
373+
type: 'string',
374+
default: '',
375+
validate: parseAssertionVerificationKeys,
376+
})
252377
.option('concurrencyLimit', {
253378
alias: 'concurrency-limit',
254379
group: 'Decrypt',
@@ -301,7 +426,7 @@ export const handleArgs = (args: string[]) => {
301426
.options({
302427
assertions: {
303428
group: 'Encrypt Options:',
304-
desc: 'ZTDF assertion config objects',
429+
desc: 'ZTDF assertion config objects or path to a json file containing ZTDF assertion config objects',
305430
type: 'string',
306431
default: '',
307432
validate: parseAssertionConfig,

lib/tdf3/src/client/builders.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,19 @@ class DecryptParamsBuilder {
666666
return this;
667667
}
668668

669+
/**
670+
* Sets the assertion verification keys for the decryption parameters.
671+
*
672+
* @param {AssertionVerificationKeys} assertionVerificationKeys - An array of assertion configurations to be set.
673+
* @returns {DecryptParamsBuilder} The current instance of the EncryptParamsBuilder for method chaining.
674+
*/
675+
withAssertionVerificationKeys(
676+
assertionVerificationKeys: AssertionVerificationKeys
677+
): DecryptParamsBuilder {
678+
this._params.assertionVerificationKeys = assertionVerificationKeys;
679+
return this;
680+
}
681+
669682
_deepCopy(_params: DecryptParams) {
670683
return freeze({ ..._params });
671684
}

lib/tests/mocha/unit/builders.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const aex = {
88
pubKey: 'PUBKEY',
99
};
1010

11-
describe('EncyptParamsBuilder', () => {
11+
describe('EncryptParamsBuilder', () => {
1212
describe('setAttributes', () => {
1313
it('should accept valid attribute', () => {
1414
const paramsBuilder = new EncryptParamsBuilder();

0 commit comments

Comments
 (0)