Skip to content

Commit 02177c8

Browse files
fix(cli): Adds --assertions to cli for testing
just a bump
1 parent 3277ecf commit 02177c8

File tree

13 files changed

+247
-238
lines changed

13 files changed

+247
-238
lines changed

cli/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/src/cli.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from '@opentdf/client';
1919
import { CLIError, Level, log } from './logger.js';
2020
import { webcrypto } from 'crypto';
21+
import * as assertions from '@opentdf/client/assertions';
2122
import { attributeFQNsAsValues } from '@opentdf/client/nano';
2223
import { base64 } from '@opentdf/client/encodings';
2324

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

123+
function parseAssertionConfig(s: string): assertions.AssertionConfig[] {
124+
const u = JSON.parse(s);
125+
// if u is null or empty, return an empty array
126+
if (!u) {
127+
return [];
128+
}
129+
const a = Array.isArray(u) ? u : [u];
130+
for (const assertion of a) {
131+
if (!assertions.isAssertionConfig(assertion)) {
132+
throw new CLIError('CRITICAL', `invalid assertion config ${JSON.stringify(assertion)}`);
133+
}
134+
}
135+
return a;
136+
}
137+
122138
async function tdf3EncryptParamsFor(argv: Partial<mainArgs>): Promise<EncryptParams> {
123139
const c = new EncryptParamsBuilder();
140+
if (argv.assertions?.length) {
141+
c.withAssertions(parseAssertionConfig(argv.assertions));
142+
}
124143
if (argv.attributes?.length) {
125144
c.setAttributes(argv.attributes.split(','));
126145
}
@@ -203,7 +222,7 @@ export const handleArgs = (args: string[]) => {
203222
group: 'Security:',
204223
desc: 'allowed KAS origins, comma separated; defaults to [kasEndpoint]',
205224
type: 'string',
206-
validate: (attributes: string) => attributes.split(','),
225+
validate: (uris: string) => uris.split(','),
207226
})
208227
.option('ignoreAllowList', {
209228
group: 'Security:',
@@ -254,6 +273,13 @@ export const handleArgs = (args: string[]) => {
254273

255274
// Policy, encryption, and container options
256275
.options({
276+
assertions: {
277+
group: 'Encrypt Options:',
278+
desc: 'ZTDF assertion config objects',
279+
type: 'string',
280+
default: '',
281+
validate: parseAssertionConfig,
282+
},
257283
attributes: {
258284
group: 'Encrypt Options:',
259285
desc: 'Data attributes for the policy',

lib/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@
2929
"require": "./dist/cjs/tdf3/index.js",
3030
"import": "./dist/web/tdf3/index.js"
3131
},
32+
"./assertions": {
33+
"default": {
34+
"types": "./dist/types/tdf3/src/assertions.d.ts",
35+
"require": "./dist/cjs/tdf3/src/assertions.js",
36+
"import": "./dist/web/tdf3/src/assertions.js"
37+
}
38+
},
3239
"./encodings": {
3340
"default": {
3441
"types": "./dist/types/src/encodings/index.d.ts",

lib/tdf3/src/assertions.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { canonicalizeEx } from 'json-canonicalize';
2+
import { SignJWT, jwtVerify } from 'jose';
3+
import { base64, hex } from '../../src/encodings/index.js';
4+
import { ConfigurationError, IntegrityError, InvalidFileError } from '../../src/errors.js';
5+
6+
export type AssertionKeyAlg = 'RS256' | 'HS256';
7+
export type AssertionType = 'handling' | 'other';
8+
export type Scope = 'tdo' | 'payload';
9+
export type AppliesToState = 'encrypted' | 'unencrypted';
10+
export type BindingMethod = 'jws';
11+
12+
// Statement type
13+
export type Statement = {
14+
format: string;
15+
schema: string;
16+
value: string;
17+
};
18+
19+
// Binding type
20+
export type Binding = {
21+
method: string;
22+
signature: string;
23+
};
24+
25+
// Assertion type
26+
export type Assertion = {
27+
id: string;
28+
type: AssertionType;
29+
scope: Scope;
30+
appliesToState?: AppliesToState;
31+
statement: Statement;
32+
binding: Binding;
33+
};
34+
35+
export type AssertionPayload = {
36+
assertionHash: string;
37+
assertionSig: string;
38+
};
39+
40+
/**
41+
* Computes the SHA-256 hash of the assertion object, excluding the 'binding' and 'hash' properties.
42+
*
43+
* @returns the hexadecimal string representation of the hash
44+
*/
45+
export async function hash(a: Assertion): Promise<string> {
46+
const result = canonicalizeEx(a, { exclude: ['binding', 'hash', 'sign', 'verify'] });
47+
48+
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(result));
49+
return hex.encodeArrayBuffer(hash);
50+
}
51+
52+
/**
53+
* Signs the given hash and signature using the provided key and sets the binding method and signature.
54+
*
55+
* @param hash - The hash to be signed.
56+
* @param sig - The signature to be signed.
57+
* @param {AssertionKey} key - The key used for signing.
58+
* @returns {Promise<void>} A promise that resolves when the signing is complete.
59+
*/
60+
async function sign(
61+
thiz: Assertion,
62+
assertionHash: string,
63+
sig: string,
64+
key: AssertionKey
65+
): Promise<Assertion> {
66+
const payload: AssertionPayload = {
67+
assertionHash,
68+
assertionSig: sig,
69+
};
70+
71+
let token: string;
72+
try {
73+
token = await new SignJWT(payload).setProtectedHeader({ alg: key.alg }).sign(key.key as any);
74+
} catch (error) {
75+
throw new ConfigurationError(`Signing assertion failed: ${error.message}`, error);
76+
}
77+
thiz.binding.method = 'jws';
78+
thiz.binding.signature = token;
79+
return thiz;
80+
}
81+
82+
// a function that takes an unknown or any object and asserts that it is or is not an AssertionConfig object
83+
export function isAssertionConfig(obj: unknown): obj is AssertionConfig {
84+
return (
85+
!!obj &&
86+
typeof obj === 'object' &&
87+
'id' in obj &&
88+
'type' in obj &&
89+
'scope' in obj &&
90+
'appliesToState' in obj &&
91+
'statement' in obj
92+
);
93+
}
94+
95+
/**
96+
* Verifies the signature of the assertion using the provided key.
97+
*
98+
* @param {AssertionKey} key - The key used for verification.
99+
* @returns {Promise<[string, string]>} A promise that resolves to a tuple containing the assertion hash and signature.
100+
* @throws {Error} If the verification fails.
101+
*/
102+
export async function verify(
103+
thiz: Assertion,
104+
aggregateHash: string,
105+
key: AssertionKey
106+
): Promise<void> {
107+
let payload: AssertionPayload;
108+
try {
109+
const uj = await jwtVerify(thiz.binding.signature, key.key as any, {
110+
algorithms: [key.alg],
111+
});
112+
payload = uj.payload as AssertionPayload;
113+
} catch (error) {
114+
throw new InvalidFileError(`Verifying assertion failed: ${error.message}`, error);
115+
}
116+
const { assertionHash, assertionSig } = payload;
117+
118+
// Get the hash of the assertion
119+
const hashOfAssertion = await hash(thiz);
120+
const combinedHash = aggregateHash + hashOfAssertion;
121+
const encodedHash = base64.encode(combinedHash);
122+
123+
// check if assertionHash is same as hashOfAssertion
124+
if (hashOfAssertion !== assertionHash) {
125+
throw new IntegrityError('Assertion hash mismatch');
126+
}
127+
128+
// check if assertionSig is same as encodedHash
129+
if (assertionSig !== encodedHash) {
130+
throw new IntegrityError('Failed integrity check on assertion signature');
131+
}
132+
}
133+
134+
/**
135+
* Creates an Assertion object with the specified properties.
136+
*/
137+
export async function CreateAssertion(
138+
aggregateHash: string,
139+
assertionConfig: AssertionConfig
140+
): Promise<Assertion> {
141+
if (!assertionConfig.signingKey) {
142+
throw new ConfigurationError('Assertion signing key is required');
143+
}
144+
145+
const a: Assertion = {
146+
id: assertionConfig.id,
147+
type: assertionConfig.type,
148+
scope: assertionConfig.scope,
149+
appliesToState: assertionConfig.appliesToState,
150+
statement: assertionConfig.statement,
151+
// empty binding
152+
binding: { method: '', signature: '' },
153+
};
154+
155+
const assertionHash = await hash(a);
156+
const combinedHash = aggregateHash + assertionHash;
157+
const encodedHash = base64.encode(combinedHash);
158+
159+
return await sign(a, assertionHash, encodedHash, assertionConfig.signingKey);
160+
}
161+
162+
export type AssertionKey = {
163+
alg: AssertionKeyAlg;
164+
key: unknown; // Replace AnyKey with the actual type of your key
165+
};
166+
167+
// AssertionConfig is a shadow of Assertion with the addition of the signing key.
168+
// It is used on creation of the assertion.
169+
export type AssertionConfig = {
170+
id: string;
171+
type: AssertionType;
172+
scope: Scope;
173+
appliesToState: AppliesToState;
174+
statement: Statement;
175+
signingKey?: AssertionKey;
176+
};
177+
178+
// AssertionVerificationKeys represents the verification keys for assertions.
179+
export type AssertionVerificationKeys = {
180+
DefaultKey?: AssertionKey;
181+
Keys: Record<string, AssertionKey>;
182+
};

lib/tdf3/src/client/AssertionConfig.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

lib/tdf3/src/client/builders.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { PemKeyPair } from '../crypto/declarations.js';
88
import { EntityObject } from '../../../src/tdf/EntityObject.js';
99
import { DecoratedReadableStream } from './DecoratedReadableStream.js';
1010
import { type Chunker } from '../utils/chunkers.js';
11-
import { AssertionConfig, AssertionVerificationKeys } from './AssertionConfig.js';
11+
import { AssertionConfig, AssertionVerificationKeys } from '../assertions.js';
1212
import { Value } from '../../../src/policy/attributes.js';
1313

1414
export const DEFAULT_SEGMENT_SIZE: number = 1024 * 1024;

0 commit comments

Comments
 (0)