Skip to content

Commit 10ff5c7

Browse files
authored
feat(sdk): Assertion support (#350)
* feat(sdk): Assertion support
1 parent e775ba5 commit 10ff5c7

File tree

10 files changed

+359
-3
lines changed

10 files changed

+359
-3
lines changed

lib/package-lock.json

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

lib/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"dpop": "^1.2.0",
7070
"eventemitter3": "^5.0.1",
7171
"jose": "^4.14.4",
72+
"json-canonicalize": "^1.0.6",
7273
"streamsaver": "^2.0.6",
7374
"uuid": "~9.0.0"
7475
},
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {
2+
AssertionKeyAlg,
3+
AssertionType,
4+
Scope,
5+
AppliesToState,
6+
Statement,
7+
} from '../models/assertion.js';
8+
9+
export type AssertionKey = {
10+
alg: AssertionKeyAlg;
11+
key: any; // Replace AnyKey with the actual type of your key
12+
};
13+
14+
// AssertionConfig is a shadow of Assertion with the addition of the signing key.
15+
// It is used on creation of the assertion.
16+
export type AssertionConfig = {
17+
id: string;
18+
type: AssertionType;
19+
scope: Scope;
20+
appliesToState: AppliesToState;
21+
statement: Statement;
22+
signingKey?: AssertionKey;
23+
};
24+
25+
// AssertionVerificationKeys represents the verification keys for assertions.
26+
export type AssertionVerificationKeys = {
27+
DefaultKey?: AssertionKey;
28+
Keys: Record<string, AssertionKey>;
29+
};

lib/tdf3/src/client/builders.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +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';
1112
import { Value } from '../../../src/policy/attributes.js';
1213

1314
export const DEFAULT_SEGMENT_SIZE: number = 1024 * 1024;
@@ -50,6 +51,7 @@ export type EncryptParams = {
5051
keyMiddleware?: EncryptKeyMiddleware;
5152
splitPlan?: SplitStep[];
5253
streamMiddleware?: EncryptStreamMiddleware;
54+
assertionConfigs?: AssertionConfig[];
5355
};
5456

5557
// 'Readonly<EncryptParams>': scope, metadata, offline, windowSize, asHtml
@@ -77,6 +79,7 @@ class EncryptParamsBuilder {
7779
offline: false,
7880
windowSize: DEFAULT_SEGMENT_SIZE,
7981
asHtml: false,
82+
assertionConfigs: [],
8083
}
8184
) {
8285
this._params = { ...params };
@@ -485,6 +488,17 @@ class EncryptParamsBuilder {
485488
build(): Readonly<EncryptParams> {
486489
return this._deepCopy(this._params as EncryptParams);
487490
}
491+
492+
/**
493+
* Sets the assertion configurations for the encryption parameters.
494+
*
495+
* @param {AssertionConfig[]} assertionConfigs - An array of assertion configurations to be set.
496+
* @returns {EncryptParamsBuilder} The current instance of the EncryptParamsBuilder for method chaining.
497+
*/
498+
withAssertions(assertionConfigs: AssertionConfig[]): EncryptParamsBuilder {
499+
this._params.assertionConfigs = assertionConfigs;
500+
return this;
501+
}
488502
}
489503

490504
export type DecryptKeyMiddleware = (key: Binary) => Promise<Binary>;
@@ -505,6 +519,7 @@ export type DecryptParams = {
505519
source: DecryptSource;
506520
keyMiddleware?: DecryptKeyMiddleware;
507521
streamMiddleware?: DecryptStreamMiddleware;
522+
assertionVerificationKeys?: AssertionVerificationKeys;
508523
};
509524

510525
/**

lib/tdf3/src/client/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ export class Client {
407407
keyMiddleware = defaultKeyMiddleware,
408408
streamMiddleware = async (stream: DecoratedReadableStream) => stream,
409409
splitPlan,
410+
assertionConfigs = [],
410411
}: EncryptParams): Promise<DecoratedReadableStream> {
411412
const dpopKeys = await this.dpopKeys;
412413

@@ -519,6 +520,7 @@ export class Client {
519520
progressHandler: this.clientConfig.progressHandler,
520521
keyForEncryption,
521522
keyForManifest,
523+
assertionConfigs,
522524
};
523525

524526
const stream = await (streamMiddleware as EncryptStreamMiddleware)(await writeStream(ecfg));
@@ -549,6 +551,7 @@ export class Client {
549551
* @param params streamMiddleware fucntion to process streamMiddleware
550552
* @param params.source A data stream object, one of remote, stream, buffer, etc. types.
551553
* @param params.eo Optional entity object (legacy AuthZ)
554+
* @param params.assertionVerificationKeys Optional verification keys for assertions.
552555
* @return a {@link https://nodejs.org/api/stream.html#stream_class_stream_readable|Readable} stream containing the decrypted plaintext.
553556
* @see DecryptParamsBuilder
554557
*/
@@ -557,6 +560,7 @@ export class Client {
557560
source,
558561
keyMiddleware = async (key: Binary) => key,
559562
streamMiddleware = async (stream: DecoratedReadableStream) => stream,
563+
assertionVerificationKeys,
560564
}: DecryptParams): Promise<DecoratedReadableStream> {
561565
const dpopKeys = await this.dpopKeys;
562566
let entityObject;
@@ -588,6 +592,7 @@ export class Client {
588592
fileStreamServiceWorker: this.clientConfig.fileStreamServiceWorker,
589593
keyMiddleware,
590594
progressHandler: this.clientConfig.progressHandler,
595+
assertionVerificationKeys,
591596
})
592597
);
593598
}

lib/tdf3/src/models/assertion.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { canonicalizeEx } from 'json-canonicalize';
2+
import { SignJWT, jwtVerify } from 'jose';
3+
import { AssertionKey } from './../client/AssertionConfig.js';
4+
5+
export type AssertionKeyAlg = 'RS256' | 'HS256';
6+
export type AssertionType = 'handling' | 'other';
7+
export type Scope = 'tdo' | 'payload';
8+
export type AppliesToState = 'encrypted' | 'unencrypted';
9+
export type BindingMethod = 'jws';
10+
11+
const kAssertionHash = 'assertionHash';
12+
const kAssertionSignature = 'assertionSig';
13+
14+
// Statement type
15+
export type Statement = {
16+
format: string;
17+
schema: string;
18+
value: string;
19+
};
20+
21+
// Binding type
22+
export type Binding = {
23+
method: string;
24+
signature: string;
25+
};
26+
27+
// Assertion type
28+
export type Assertion = {
29+
id: string;
30+
type: AssertionType;
31+
scope: Scope;
32+
appliesToState?: AppliesToState;
33+
statement: Statement;
34+
binding: Binding;
35+
hash: () => Promise<string>;
36+
sign: (hash: string, sig: string, key: AssertionKey) => Promise<void>;
37+
verify: (key: AssertionKey) => Promise<[string, string]>;
38+
};
39+
40+
/**
41+
* Computes the SHA-256 hash of the assertion object, excluding the 'binding' and 'hash' properties.
42+
*
43+
* @returns {Promise<string>} A promise that resolves to the hexadecimal string representation of the hash.
44+
*/
45+
export async function hash(this: Assertion): Promise<string> {
46+
const result = canonicalizeEx(this, { exclude: ['binding', 'hash', 'sign', 'verify'] });
47+
48+
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(result));
49+
return Buffer.from(hash).toString('hex');
50+
}
51+
52+
/**
53+
* Signs the given hash and signature using the provided key and sets the binding method and signature.
54+
*
55+
* @param {string} hash - The hash to be signed.
56+
* @param {string} 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+
export async function sign(
61+
this: Assertion,
62+
assertionHash: string,
63+
sig: string,
64+
key: AssertionKey
65+
): Promise<void> {
66+
const payload: any = {};
67+
payload[kAssertionHash] = assertionHash;
68+
payload[kAssertionSignature] = sig;
69+
70+
try {
71+
const token = await new SignJWT(payload).setProtectedHeader({ alg: key.alg }).sign(key.key);
72+
73+
this.binding.method = 'jws';
74+
this.binding.signature = token;
75+
} catch (error) {
76+
throw new Error(`Signing assertion failed: ${error.message}`);
77+
}
78+
}
79+
80+
/**
81+
* Verifies the signature of the assertion using the provided key.
82+
*
83+
* @param {AssertionKey} key - The key used for verification.
84+
* @returns {Promise<[string, string]>} A promise that resolves to a tuple containing the assertion hash and signature.
85+
* @throws {Error} If the verification fails.
86+
*/
87+
export async function verify(this: Assertion, key: AssertionKey): Promise<[string, string]> {
88+
try {
89+
const { payload } = await jwtVerify(this.binding.signature, key.key, {
90+
algorithms: [key.alg],
91+
});
92+
93+
return [payload[kAssertionHash] as string, payload[kAssertionSignature] as string];
94+
} catch (error) {
95+
throw new Error(`Verifying assertion failed: ${error.message}`);
96+
}
97+
}
98+
99+
/**
100+
* Creates an Assertion object with the specified properties.
101+
*
102+
* @param {string} id - The unique identifier for the assertion.
103+
* @param {AssertionType} type - The type of the assertion (e.g., 'handling', 'other').
104+
* @param {Scope} scope - The scope of the assertion (e.g., 'tdo', 'payload').
105+
* @param {Statement} statement - The statement associated with the assertion.
106+
* @param {Binding} binding - The binding method and signature for the assertion.
107+
* @param {AppliesToState} [appliesToState] - The state to which the assertion applies (optional).
108+
* @returns {Assertion} The created Assertion object.
109+
*/
110+
export function CreateAssertion(
111+
id: string,
112+
type: AssertionType,
113+
scope: Scope,
114+
statement: Statement,
115+
appliesToState?: AppliesToState,
116+
binding?: Binding
117+
): Assertion {
118+
return {
119+
id,
120+
type,
121+
scope,
122+
appliesToState,
123+
statement,
124+
binding: { method: binding?.method ?? '', signature: binding?.signature ?? '' },
125+
hash,
126+
sign,
127+
verify,
128+
};
129+
}

lib/tdf3/src/models/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from './manifest.js';
55
export * from './payload.js';
66
export * from './policy.js';
77
export * from './upsert-response.js';
8+
export * from './assertion.js';

lib/tdf3/src/models/manifest.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { type Assertion } from './assertion.js';
12
import { type Payload } from './payload.js';
23
import { type EncryptionInformation } from './encryption-information.js';
34

45
export type Manifest = {
56
payload: Payload;
67
encryptionInformation: EncryptionInformation;
8+
assertions: Assertion[];
79
};

0 commit comments

Comments
 (0)