@@ -22,6 +22,7 @@ import { webcrypto } from 'crypto';
22
22
import * as assertions from '@opentdf/sdk/assertions' ;
23
23
import { attributeFQNsAsValues } from '@opentdf/sdk/nano' ;
24
24
import { base64 } from '@opentdf/sdk/encodings' ;
25
+ import { importPKCS8 , importSPKI , KeyLike } from 'jose' ; // for RS256
25
26
26
27
type AuthToProcess = {
27
28
auth ?: string ;
@@ -120,11 +121,71 @@ function addParams(client: AnyNanoClient, argv: Partial<mainArgs>) {
120
121
log ( 'SILLY' , `Built encrypt params dissems: ${ client . dissems } , attrs: ${ client . dataAttributes } ` ) ;
121
122
}
122
123
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
+
123
179
async function tdf3DecryptParamsFor ( argv : Partial < mainArgs > ) : Promise < DecryptParams > {
124
180
const c = new DecryptParamsBuilder ( ) ;
125
181
if ( argv . noVerifyAssertions ) {
126
182
c . withNoVerifyAssertions ( true ) ;
127
183
}
184
+ if ( argv . assertionVerificationKeys ) {
185
+ c . withAssertionVerificationKeys (
186
+ await parseAssertionVerificationKeys ( argv . assertionVerificationKeys )
187
+ ) ;
188
+ }
128
189
if ( argv . concurrencyLimit ) {
129
190
c . withConcurrencyLimit ( argv . concurrencyLimit ) ;
130
191
} else {
@@ -134,8 +195,52 @@ async function tdf3DecryptParamsFor(argv: Partial<mainArgs>): Promise<DecryptPar
134
195
return c . build ( ) ;
135
196
}
136
197
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
+
139
244
// if u is null or empty, return an empty array
140
245
if ( ! u ) {
141
246
return [ ] ;
@@ -145,14 +250,26 @@ function parseAssertionConfig(s: string): assertions.AssertionConfig[] {
145
250
if ( ! assertions . isAssertionConfig ( assertion ) ) {
146
251
throw new CLIError ( 'CRITICAL' , `invalid assertion config ${ JSON . stringify ( assertion ) } ` ) ;
147
252
}
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
+ }
148
265
}
149
266
return a ;
150
267
}
151
268
152
269
async function tdf3EncryptParamsFor ( argv : Partial < mainArgs > ) : Promise < EncryptParams > {
153
270
const c = new EncryptParamsBuilder ( ) ;
154
271
if ( argv . assertions ?. length ) {
155
- c . withAssertions ( parseAssertionConfig ( argv . assertions ) ) ;
272
+ c . withAssertions ( await parseAssertionConfig ( argv . assertions ) ) ;
156
273
}
157
274
if ( argv . attributes ?. length ) {
158
275
c . setAttributes ( argv . attributes . split ( ',' ) ) ;
@@ -249,6 +366,14 @@ export const handleArgs = (args: string[]) => {
249
366
desc : 'Do not verify assertions' ,
250
367
type : 'boolean' ,
251
368
} )
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
+ } )
252
377
. option ( 'concurrencyLimit' , {
253
378
alias : 'concurrency-limit' ,
254
379
group : 'Decrypt' ,
@@ -301,7 +426,7 @@ export const handleArgs = (args: string[]) => {
301
426
. options ( {
302
427
assertions : {
303
428
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 ' ,
305
430
type : 'string' ,
306
431
default : '' ,
307
432
validate : parseAssertionConfig ,
0 commit comments