Skip to content

Commit

Permalink
fix(cli): Adds --assertions to cli for testing
Browse files Browse the repository at this point in the history
just a bump
  • Loading branch information
dmihalcik-virtru committed Oct 23, 2024
1 parent 3277ecf commit 02177c8
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 238 deletions.
8 changes: 4 additions & 4 deletions cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 27 additions & 1 deletion cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '@opentdf/client';
import { CLIError, Level, log } from './logger.js';
import { webcrypto } from 'crypto';
import * as assertions from '@opentdf/client/assertions';
import { attributeFQNsAsValues } from '@opentdf/client/nano';
import { base64 } from '@opentdf/client/encodings';

Expand Down Expand Up @@ -119,8 +120,26 @@ async function tdf3DecryptParamsFor(argv: Partial<mainArgs>): Promise<DecryptPar
return c.build();
}

function parseAssertionConfig(s: string): assertions.AssertionConfig[] {
const u = JSON.parse(s);
// if u is null or empty, return an empty array
if (!u) {
return [];
}
const a = Array.isArray(u) ? u : [u];
for (const assertion of a) {
if (!assertions.isAssertionConfig(assertion)) {
throw new CLIError('CRITICAL', `invalid assertion config ${JSON.stringify(assertion)}`);
}
}
return a;
}

async function tdf3EncryptParamsFor(argv: Partial<mainArgs>): Promise<EncryptParams> {
const c = new EncryptParamsBuilder();
if (argv.assertions?.length) {
c.withAssertions(parseAssertionConfig(argv.assertions));
}
if (argv.attributes?.length) {
c.setAttributes(argv.attributes.split(','));
}
Expand Down Expand Up @@ -203,7 +222,7 @@ export const handleArgs = (args: string[]) => {
group: 'Security:',
desc: 'allowed KAS origins, comma separated; defaults to [kasEndpoint]',
type: 'string',
validate: (attributes: string) => attributes.split(','),
validate: (uris: string) => uris.split(','),
})
.option('ignoreAllowList', {
group: 'Security:',
Expand Down Expand Up @@ -254,6 +273,13 @@ export const handleArgs = (args: string[]) => {

// Policy, encryption, and container options
.options({
assertions: {
group: 'Encrypt Options:',
desc: 'ZTDF assertion config objects',
type: 'string',
default: '',
validate: parseAssertionConfig,
},
attributes: {
group: 'Encrypt Options:',
desc: 'Data attributes for the policy',
Expand Down
7 changes: 7 additions & 0 deletions lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@
"require": "./dist/cjs/tdf3/index.js",
"import": "./dist/web/tdf3/index.js"
},
"./assertions": {
"default": {
"types": "./dist/types/tdf3/src/assertions.d.ts",
"require": "./dist/cjs/tdf3/src/assertions.js",
"import": "./dist/web/tdf3/src/assertions.js"
}
},
"./encodings": {
"default": {
"types": "./dist/types/src/encodings/index.d.ts",
Expand Down
182 changes: 182 additions & 0 deletions lib/tdf3/src/assertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { canonicalizeEx } from 'json-canonicalize';
import { SignJWT, jwtVerify } from 'jose';
import { base64, hex } from '../../src/encodings/index.js';
import { ConfigurationError, IntegrityError, InvalidFileError } from '../../src/errors.js';

export type AssertionKeyAlg = 'RS256' | 'HS256';
export type AssertionType = 'handling' | 'other';
export type Scope = 'tdo' | 'payload';
export type AppliesToState = 'encrypted' | 'unencrypted';
export type BindingMethod = 'jws';

// Statement type
export type Statement = {
format: string;
schema: string;
value: string;
};

// Binding type
export type Binding = {
method: string;
signature: string;
};

// Assertion type
export type Assertion = {
id: string;
type: AssertionType;
scope: Scope;
appliesToState?: AppliesToState;
statement: Statement;
binding: Binding;
};

export type AssertionPayload = {
assertionHash: string;
assertionSig: string;
};

/**
* Computes the SHA-256 hash of the assertion object, excluding the 'binding' and 'hash' properties.
*
* @returns the hexadecimal string representation of the hash
*/
export async function hash(a: Assertion): Promise<string> {
const result = canonicalizeEx(a, { exclude: ['binding', 'hash', 'sign', 'verify'] });

const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(result));
return hex.encodeArrayBuffer(hash);
}

/**
* Signs the given hash and signature using the provided key and sets the binding method and signature.
*
* @param hash - The hash to be signed.
* @param sig - The signature to be signed.
* @param {AssertionKey} key - The key used for signing.
* @returns {Promise<void>} A promise that resolves when the signing is complete.
*/
async function sign(
thiz: Assertion,
assertionHash: string,
sig: string,
key: AssertionKey
): Promise<Assertion> {
const payload: AssertionPayload = {
assertionHash,
assertionSig: sig,
};

let token: string;
try {
token = await new SignJWT(payload).setProtectedHeader({ alg: key.alg }).sign(key.key as any);

Check failure on line 73 in lib/tdf3/src/assertions.ts

View workflow job for this annotation

GitHub Actions / lib

Unexpected any. Specify a different type
} catch (error) {
throw new ConfigurationError(`Signing assertion failed: ${error.message}`, error);
}
thiz.binding.method = 'jws';
thiz.binding.signature = token;
return thiz;
}

// a function that takes an unknown or any object and asserts that it is or is not an AssertionConfig object
export function isAssertionConfig(obj: unknown): obj is AssertionConfig {
return (
!!obj &&
typeof obj === 'object' &&
'id' in obj &&
'type' in obj &&
'scope' in obj &&
'appliesToState' in obj &&
'statement' in obj
);
}

/**
* Verifies the signature of the assertion using the provided key.
*
* @param {AssertionKey} key - The key used for verification.
* @returns {Promise<[string, string]>} A promise that resolves to a tuple containing the assertion hash and signature.
* @throws {Error} If the verification fails.
*/
export async function verify(
thiz: Assertion,
aggregateHash: string,
key: AssertionKey
): Promise<void> {
let payload: AssertionPayload;
try {
const uj = await jwtVerify(thiz.binding.signature, key.key as any, {

Check failure on line 109 in lib/tdf3/src/assertions.ts

View workflow job for this annotation

GitHub Actions / lib

Unexpected any. Specify a different type
algorithms: [key.alg],
});
payload = uj.payload as AssertionPayload;
} catch (error) {
throw new InvalidFileError(`Verifying assertion failed: ${error.message}`, error);
}
const { assertionHash, assertionSig } = payload;

// Get the hash of the assertion
const hashOfAssertion = await hash(thiz);
const combinedHash = aggregateHash + hashOfAssertion;
const encodedHash = base64.encode(combinedHash);

// check if assertionHash is same as hashOfAssertion
if (hashOfAssertion !== assertionHash) {
throw new IntegrityError('Assertion hash mismatch');
}

// check if assertionSig is same as encodedHash
if (assertionSig !== encodedHash) {
throw new IntegrityError('Failed integrity check on assertion signature');
}
}

/**
* Creates an Assertion object with the specified properties.
*/
export async function CreateAssertion(
aggregateHash: string,
assertionConfig: AssertionConfig
): Promise<Assertion> {
if (!assertionConfig.signingKey) {
throw new ConfigurationError('Assertion signing key is required');
}

const a: Assertion = {
id: assertionConfig.id,
type: assertionConfig.type,
scope: assertionConfig.scope,
appliesToState: assertionConfig.appliesToState,
statement: assertionConfig.statement,
// empty binding
binding: { method: '', signature: '' },
};

const assertionHash = await hash(a);
const combinedHash = aggregateHash + assertionHash;
const encodedHash = base64.encode(combinedHash);

return await sign(a, assertionHash, encodedHash, assertionConfig.signingKey);
}

export type AssertionKey = {
alg: AssertionKeyAlg;
key: unknown; // Replace AnyKey with the actual type of your key
};

// AssertionConfig is a shadow of Assertion with the addition of the signing key.
// It is used on creation of the assertion.
export type AssertionConfig = {
id: string;
type: AssertionType;
scope: Scope;
appliesToState: AppliesToState;
statement: Statement;
signingKey?: AssertionKey;
};

// AssertionVerificationKeys represents the verification keys for assertions.
export type AssertionVerificationKeys = {
DefaultKey?: AssertionKey;
Keys: Record<string, AssertionKey>;
};
29 changes: 0 additions & 29 deletions lib/tdf3/src/client/AssertionConfig.ts

This file was deleted.

2 changes: 1 addition & 1 deletion lib/tdf3/src/client/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { PemKeyPair } from '../crypto/declarations.js';
import { EntityObject } from '../../../src/tdf/EntityObject.js';
import { DecoratedReadableStream } from './DecoratedReadableStream.js';
import { type Chunker } from '../utils/chunkers.js';
import { AssertionConfig, AssertionVerificationKeys } from './AssertionConfig.js';
import { AssertionConfig, AssertionVerificationKeys } from '../assertions.js';
import { Value } from '../../../src/policy/attributes.js';

export const DEFAULT_SEGMENT_SIZE: number = 1024 * 1024;
Expand Down
Loading

0 comments on commit 02177c8

Please sign in to comment.