Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Assertion signing key handling and verification in the CLI #409

Merged
merged 1 commit into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 129 additions & 4 deletions cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { webcrypto } from 'crypto';
import * as assertions from '@opentdf/sdk/assertions';
import { attributeFQNsAsValues } from '@opentdf/sdk/nano';
import { base64 } from '@opentdf/sdk/encodings';
import { importPKCS8, importSPKI, KeyLike } from 'jose'; // for RS256

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

async function parseAssertionVerificationKeys(
s: string
): Promise<assertions.AssertionVerificationKeys> {
let u;
try {
u = JSON.parse(s);
} catch (err) {
// try as file name:
try {
const jsonFile = await openAsBlob(s);
u = JSON.parse(await jsonFile.text());
} catch (err2) {
throw new CLIError(
'CRITICAL',
`Failed to open/parse assertion verification keys as string or file path ${err.message}`,
err
);
}
}
if (typeof u !== 'object' || u === null) {
throw new Error('Invalid input: The input must be an object');
}
// handle both cases of "keys"
if (!('Keys' in u && typeof u.Keys === 'object')) {
if ('keys' in u && typeof u.keys === 'object') {
u.Keys = u.keys;
} else {
throw new CLIError(
'CRITICAL',
'Invalid input: invalid structure of assertionVerificationKeys'
);
}
}
for (const assertionName in u.Keys) {
const assertionKey = u.Keys[assertionName];
// Ensure each entry has the required 'key' and 'alg' fields
if (typeof assertionKey !== 'object' || assertionKey === null) {
throw new CLIError('CRITICAL', `Invalid assertion for ${assertionName}: Must be an object`);
}

if (typeof assertionKey.key !== 'string' || typeof assertionKey.alg !== 'string') {
throw new CLIError(
'CRITICAL',
`Invalid assertion for ${assertionName}: Missing or invalid 'key' or 'alg'`
);
}
try {
u.Keys[assertionName].key = await correctAssertionKeys(assertionKey.alg, assertionKey.key);
} catch (err) {
throw new CLIError('CRITICAL', `Issue converting assertion key from string: ${err.message}`);
}
}
return u;
}

async function tdf3DecryptParamsFor(argv: Partial<mainArgs>): Promise<DecryptParams> {
const c = new DecryptParamsBuilder();
if (argv.noVerifyAssertions) {
c.withNoVerifyAssertions(true);
}
if (argv.assertionVerificationKeys) {
c.withAssertionVerificationKeys(
await parseAssertionVerificationKeys(argv.assertionVerificationKeys)
);
}
if (argv.concurrencyLimit) {
c.withConcurrencyLimit(argv.concurrencyLimit);
} else {
Expand All @@ -134,8 +195,52 @@ async function tdf3DecryptParamsFor(argv: Partial<mainArgs>): Promise<DecryptPar
return c.build();
}

function parseAssertionConfig(s: string): assertions.AssertionConfig[] {
const u = JSON.parse(s);
async function correctAssertionKeys(
alg: string,
key: KeyLike | Uint8Array
): Promise<KeyLike | Uint8Array> {
if (alg === 'HS256') {
// Convert key string to Uint8Array
if (typeof key !== 'string') {
throw new CLIError('CRITICAL', 'HS256 key must be a string');
}
return new TextEncoder().encode(key); // Update array element directly
} else if (alg === 'RS256') {
// Convert PEM string to a KeyLike object
if (typeof key !== 'string') {
throw new CLIError('CRITICAL', 'RS256 key must be a PEM string');
}
try {
return await importPKCS8(key, 'RS256'); // Import private key
} catch (err) {
// If importing as a private key fails, try importing as a public key
try {
return await importSPKI(key, 'RS256'); // Import public key
} catch (err) {}
}
}
// Otherwise its an unsupported alg
throw new CLIError('CRITICAL', `Unsupported signing key algorithm: ${alg}`); // Handle unsupported algs
}

async function parseAssertionConfig(s: string): Promise<assertions.AssertionConfig[]> {
let u;
try {
u = JSON.parse(s);
} catch (err) {
// try as file name:
try {
const jsonFile = await openAsBlob(s);
u = JSON.parse(await jsonFile.text());
} catch (err2) {
throw new CLIError(
'CRITICAL',
`Failed to open/parse assertions as string or file path ${err.message}`,
err
);
}
}

// if u is null or empty, return an empty array
if (!u) {
return [];
Expand All @@ -145,14 +250,26 @@ function parseAssertionConfig(s: string): assertions.AssertionConfig[] {
if (!assertions.isAssertionConfig(assertion)) {
throw new CLIError('CRITICAL', `invalid assertion config ${JSON.stringify(assertion)}`);
}
if (assertion.signingKey) {
const { alg, key } = assertion.signingKey;
try {
assertion.signingKey.key = await correctAssertionKeys(alg, key);
} catch (err) {
throw new CLIError(
'CRITICAL',
`Issue converting assertion key from string: ${err.message}`,
err
);
}
}
}
return a;
}

async function tdf3EncryptParamsFor(argv: Partial<mainArgs>): Promise<EncryptParams> {
const c = new EncryptParamsBuilder();
if (argv.assertions?.length) {
c.withAssertions(parseAssertionConfig(argv.assertions));
c.withAssertions(await parseAssertionConfig(argv.assertions));
}
if (argv.attributes?.length) {
c.setAttributes(argv.attributes.split(','));
Expand Down Expand Up @@ -249,6 +366,14 @@ export const handleArgs = (args: string[]) => {
desc: 'Do not verify assertions',
type: 'boolean',
})
.option('assertionVerificationKeys', {
alias: 'with-assertion-verification-keys',
group: 'Decrypt',
desc: 'keys for assertion verification or path to a json file containing keys for assertion verification',
type: 'string',
default: '',
validate: parseAssertionVerificationKeys,
})
.option('concurrencyLimit', {
alias: 'concurrency-limit',
group: 'Decrypt',
Expand Down Expand Up @@ -301,7 +426,7 @@ export const handleArgs = (args: string[]) => {
.options({
assertions: {
group: 'Encrypt Options:',
desc: 'ZTDF assertion config objects',
desc: 'ZTDF assertion config objects or path to a json file containing ZTDF assertion config objects',
type: 'string',
default: '',
validate: parseAssertionConfig,
Expand Down
13 changes: 13 additions & 0 deletions lib/tdf3/src/client/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,19 @@ class DecryptParamsBuilder {
return this;
}

/**
* Sets the assertion verification keys for the decryption parameters.
*
* @param {AssertionVerificationKeys} assertionVerificationKeys - An array of assertion configurations to be set.
* @returns {DecryptParamsBuilder} The current instance of the EncryptParamsBuilder for method chaining.
*/
withAssertionVerificationKeys(
assertionVerificationKeys: AssertionVerificationKeys
): DecryptParamsBuilder {
this._params.assertionVerificationKeys = assertionVerificationKeys;
return this;
}

_deepCopy(_params: DecryptParams) {
return freeze({ ..._params });
}
Expand Down
2 changes: 1 addition & 1 deletion lib/tests/mocha/unit/builders.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const aex = {
pubKey: 'PUBKEY',
};

describe('EncyptParamsBuilder', () => {
describe('EncryptParamsBuilder', () => {
describe('setAttributes', () => {
it('should accept valid attribute', () => {
const paramsBuilder = new EncryptParamsBuilder();
Expand Down
Loading