diff --git a/cli/bin/opentdf.bats b/cli/bin/opentdf.bats index a0a4849b2..0d100b19d 100755 --- a/cli/bin/opentdf.bats +++ b/cli/bin/opentdf.bats @@ -6,10 +6,53 @@ [[ $output == *"Not enough"* ]] } -@test "requires optional arguments" { +# `encrypt` validates in stages: flags, then oidcEndpoint, then the input file, +# then auth. Each of the next few tests pins one stage by checking that the +# later stages have not run yet -- the assertion on what is *absent* is the one +# that keeps the ordering from drifting back. + +@test "rejects an unknown integrity algorithm before anything else" { + run $BATS_TEST_DIRNAME/opentdf.mjs --segmentIntegrityAlgorithm bogus encrypt noone + echo "$output" + [[ $output == *"Invalid values"* ]] + [[ $output == *"bogus"* ]] + [[ $output != *"noone"* ]] +} + +@test "rejects a GMAC root signature before touching the file" { + run $BATS_TEST_DIRNAME/opentdf.mjs --rootIntegrityAlgorithm gmac encrypt noone + echo "$output" + [ "$status" -eq 1 ] + [[ $output == *"unsupported root integrity algorithm"* ]] + [[ $output != *"noone"* ]] +} + +@test "requires an oidcEndpoint before opening the input file" { run $BATS_TEST_DIRNAME/opentdf.mjs encrypt noone echo "$output" - [[ $output == *"must be specified"* ]] + [ "$status" -eq 1 ] + [[ $output == *"oidcEndpoint must be specified"* ]] + [[ $output != *"not accessable"* ]] +} + +@test "fails on an unreadable input file" { + run $BATS_TEST_DIRNAME/opentdf.mjs --kasEndpoint "https://example.com" --oidcEndpoint "http://invalid" --concurrencyLimit 1 --auth "b:c" encrypt noone + echo "$output" + [ "$status" -eq 1 ] + [[ $output == *"File is not accessable [noone]"* ]] +} + +# Integrity algorithms are accepted in any casing but must reach the manifest +# uppercase, since readers match the spec's spelling exactly. This runs against a +# closed local port, so it gets as far as the create options and then fails to +# reach the KAS -- hence no $status assertion. +@test "normalizes integrity algorithm casing" { + echo "hello" > "$BATS_TEST_TMPDIR/plain.txt" + run $BATS_TEST_DIRNAME/opentdf.mjs --kasEndpoint "http://localhost:9999" --oidcEndpoint "http://localhost:9999" --concurrencyLimit 1 --auth "b:c" --log-level debug --output "$BATS_TEST_TMPDIR/out.tdf" --segmentIntegrityAlgorithm gmac --rootIntegrityAlgorithm HS256 encrypt "$BATS_TEST_TMPDIR/plain.txt" + echo "$output" + [[ $output != *"Invalid values"* ]] + [[ $output == *"\"rootIntegrityAlgorithm\":\"HS256\""* ]] + [[ $output == *"\"segmentIntegrityAlgorithm\":\"GMAC\""* ]] } @test "fails with missing file arguments" { diff --git a/cli/src/cli.ts b/cli/src/cli.ts index 0f0a56998..c24a33f64 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -45,6 +45,13 @@ class InvalidAuthProvider { const containerTypes = ['tdf3', 'ztdf']; +/** Accepted values for the two integrity-algorithm flags, lowercase by convention. */ +const integrityAlgorithms = ['hs256', 'gmac'] as const; + +/** Flag values are case-insensitive; normalize before yargs checks `choices`. */ +const lowercaseIntegrityAlgorithm = (v: unknown) => + typeof v === 'string' ? v.toLowerCase() : (v as string); + const parseJwt = (jwt: string, field = 1) => { return JSON.parse(base64.decode(jwt.split('.')[field])); }; @@ -270,11 +277,22 @@ async function parseCreateOptions(argv: Partial): Promise): Promise { - const c: CreateTDFOptions = await parseCreateOptions(argv); - if (argv.assertions?.length) { - c.assertionConfigs = await parseAssertionConfig(argv.assertions); - } +/** + * The subset of `CreateTDFOptions` derived purely from flags -- no filesystem, no + * network. Split out so `encrypt` can reject a bad flag before it opens anything. + * Synchronous on purpose: the missing `async` is the contract. + */ +type CreateTDFFlags = Pick< + CreateTDFOptions, + | 'wrappingKeyAlgorithm' + | 'mimeType' + | 'tdfSpecVersion' + | 'rootIntegrityAlgorithm' + | 'segmentIntegrityAlgorithm' +>; + +function parseCreateTDFFlags(argv: Partial): CreateTDFFlags { + const c: CreateTDFFlags = {}; if (argv.encapKeyType?.length) { if (!isPublicKeyAlgorithm(argv.encapKeyType)) { throw new CLIError('CRITICAL', `Unsupported rewrap key algorithm: [${argv.encapKeyType}]`); @@ -291,6 +309,39 @@ async function parseCreateTDFOptions(argv: Partial): Promise, + flags: CreateTDFFlags = parseCreateTDFFlags(argv) +): Promise { + const c: CreateTDFOptions = { ...(await parseCreateOptions(argv)), ...flags }; + if (argv.assertions?.length) { + // Reads from disk (parseAssertionConfig falls back to openAsBlob), so it + // stays here rather than in the pure pass. + c.assertionConfigs = await parseAssertionConfig(argv.assertions); + } log('DEBUG', `CreateTDFOptions: ${JSON.stringify(c)}`); return c; } @@ -472,6 +523,24 @@ export const handleArgs = (args: string[]) => { type: 'string', default: '', }, + rootIntegrityAlgorithm: { + alias: 'root-integrity-algorithm', + group: 'Encrypt Options:', + desc: 'Algorithm for the root signature. Only hs256 is supported; gmac is rejected', + type: 'string', + choices: integrityAlgorithms, + coerce: lowercaseIntegrityAlgorithm, + default: 'hs256', + }, + segmentIntegrityAlgorithm: { + alias: 'segment-integrity-algorithm', + group: 'Encrypt Options:', + desc: 'Algorithm for per-segment integrity', + type: 'string', + choices: integrityAlgorithms, + coerce: lowercaseIntegrityAlgorithm, + default: 'gmac', + }, rewrapKeyType: { alias: 'rewrap-encapsulation-algorithm', group: 'Decrypt Options:', @@ -624,6 +693,16 @@ export const handleArgs = (args: string[]) => { }, async (argv) => { log('DEBUG', 'Running encrypt command'); + // Order is deliberate and covered by bin/opentdf.bats: + // 1. pure flag checks -- no I/O, so a typo fails instantly + // 2. oidcEndpoint, mirroring `decrypt` + // 3. open the input file and any assertion files + // 4. the auth provider, which may reach the IdP + const createFlags = parseCreateTDFFlags(argv); + if (!argv.oidcEndpoint) { + throw new CLIError('CRITICAL', 'oidcEndpoint must be specified'); + } + const createOptions = await parseCreateTDFOptions(argv, createFlags); const authProvider = await processAuth(argv); log('DEBUG', `Initialized auth provider ${JSON.stringify(authProvider)}`); const guessedPolicyEndpoint = guessPolicyUrl(argv); @@ -640,7 +719,7 @@ export const handleArgs = (args: string[]) => { try { log('SILLY', `Initialized client`); log('DEBUG', `TDF Create`); - const ct: DecoratedStream = await client.createTDF(await parseCreateTDFOptions(argv)); + const ct: DecoratedStream = await client.createTDF(createOptions); if (!ct) { throw new CLIError('CRITICAL', 'Encrypt configuration error: No output?'); } diff --git a/lib/src/opentdf.ts b/lib/src/opentdf.ts index 730880522..734042e1f 100644 --- a/lib/src/opentdf.ts +++ b/lib/src/opentdf.ts @@ -31,6 +31,8 @@ import { InspectedTDFOverview, loadTDFStream, type IntegrityAlgorithm, + type RootIntegrityAlgorithm, + type SegmentIntegrityAlgorithm, } from '../tdf3/src/tdf.js'; import { base64 } from './encodings/index.js'; import { Policy } from '../tdf3/src/models/policy.js'; @@ -44,7 +46,9 @@ export { type KeyAccessObject, type Manifest, type Payload, + type RootIntegrityAlgorithm, type Segment, + type SegmentIntegrityAlgorithm, type SplitType, PUBLIC_KEY_ALGORITHMS, isPublicKeyAlgorithm, @@ -130,6 +134,19 @@ export type CreateZTDFOptions = CreateOptions & { /** TDF spec version to target. */ tdfSpecVersion?: '4.2.2' | '4.3.0'; + + /** + * Algorithm used for the root signature over the aggregate of segment + * hashes. `HS256` only. Defaults to `HS256`. + */ + rootIntegrityAlgorithm?: RootIntegrityAlgorithm; + + /** + * Algorithm used for each segment's integrity value. `GMAC` reads out the + * AES-GCM tag the cipher already produced for that segment; `HS256` computes + * an HMAC over the segment ciphertext. Defaults to `GMAC`. + */ + segmentIntegrityAlgorithm?: SegmentIntegrityAlgorithm; }; /** Options for creating a TDF. */ @@ -392,6 +409,8 @@ export class OpenTDF { windowSize: opts.windowSize, wrappingKeyAlgorithm: opts.wrappingKeyAlgorithm, tdfSpecVersion: opts.tdfSpecVersion, + rootIntegrityAlgorithm: opts.rootIntegrityAlgorithm, + segmentIntegrityAlgorithm: opts.segmentIntegrityAlgorithm, }); const stream: DecoratedStream = oldStream.stream; stream.manifest = Promise.resolve(oldStream.manifest); diff --git a/lib/tdf3/src/client/builders.ts b/lib/tdf3/src/client/builders.ts index 863781ec1..20285559f 100644 --- a/lib/tdf3/src/client/builders.ts +++ b/lib/tdf3/src/client/builders.ts @@ -1,6 +1,10 @@ import { validateAttribute, validateAttributeObject } from './validation.js'; import { AttributeObject, KeyInfo, Policy } from '../models/index.js'; -import { type Metadata } from '../tdf.js'; +import { + type Metadata, + type RootIntegrityAlgorithm, + type SegmentIntegrityAlgorithm, +} from '../tdf.js'; import { Binary } from '../binary.js'; import { ConfigurationError } from '../../../src/errors.js'; @@ -57,6 +61,9 @@ export type EncryptParams = { // Preferred wrapping key algorithm. Used when KID resolution is not available. wrappingKeyAlgorithm?: KasPublicKeyAlgorithm; + rootIntegrityAlgorithm?: RootIntegrityAlgorithm; + segmentIntegrityAlgorithm?: SegmentIntegrityAlgorithm; + // Unsupported asHtml?: boolean; // Unsupported diff --git a/lib/tdf3/src/client/index.ts b/lib/tdf3/src/client/index.ts index f8438a11a..041747bfe 100644 --- a/lib/tdf3/src/client/index.ts +++ b/lib/tdf3/src/client/index.ts @@ -9,8 +9,12 @@ import { buildKeyAccess, type EncryptConfiguration, fetchKasPublicKey, + isRootIntegrityAlgorithm, + isSegmentIntegrityAlgorithm, loadTDFStream, readStream, + ROOT_INTEGRITY_ALGORITHM, + SEGMENT_INTEGRITY_ALGORITHM, validatePolicyObject, writeStream, } from '../tdf.js'; @@ -544,7 +548,19 @@ export class Client { streamMiddleware = async (stream: DecoratedReadableStream) => stream, tdfSpecVersion, wrappingKeyAlgorithm, + rootIntegrityAlgorithm = ROOT_INTEGRITY_ALGORITHM, + segmentIntegrityAlgorithm = SEGMENT_INTEGRITY_ALGORITHM, } = opts; + if (!isRootIntegrityAlgorithm(rootIntegrityAlgorithm)) { + throw new ConfigurationError( + `unsupported root integrity algorithm [${rootIntegrityAlgorithm}]; only [${ROOT_INTEGRITY_ALGORITHM}] is supported` + ); + } + if (!isSegmentIntegrityAlgorithm(segmentIntegrityAlgorithm)) { + throw new ConfigurationError( + `unsupported segment integrity algorithm [${segmentIntegrityAlgorithm}]` + ); + } const keyMiddleware = keyMiddlewareOpt ?? (() => defaultKeyMiddleware(this.cryptoService)); const scope = opts.scope ?? { attributes: [], dissem: [] }; @@ -772,8 +788,8 @@ export class Client { dpopKeys, encryptionInformation, segmentSizeDefault: windowSize, - integrityAlgorithm: 'HS256', - segmentIntegrityAlgorithm: 'GMAC', + rootIntegrityAlgorithm, + segmentIntegrityAlgorithm, contentStream: opts.source, mimeType, policy: policyObject, diff --git a/lib/tdf3/src/models/encryption-information.ts b/lib/tdf3/src/models/encryption-information.ts index 845a9b53d..951b86051 100644 --- a/lib/tdf3/src/models/encryption-information.ts +++ b/lib/tdf3/src/models/encryption-information.ts @@ -9,7 +9,11 @@ import { type EncryptResult, type SymmetricKey, } from '../crypto/declarations.js'; -import { IntegrityAlgorithm } from '../tdf.js'; +import { + ROOT_INTEGRITY_ALGORITHM, + SEGMENT_INTEGRITY_ALGORITHM, + type SegmentIntegrityAlgorithm, +} from '../tdf.js'; import { ConfigurationError } from '../../../src/errors.js'; export type KeyInfo = { @@ -32,10 +36,10 @@ export type EncryptionInformation = { readonly keyAccess: KeyAccessObject[]; readonly integrityInformation: { readonly rootSignature: { - alg: IntegrityAlgorithm; + alg: SegmentIntegrityAlgorithm; sig: string; }; - segmentHashAlg?: IntegrityAlgorithm; + segmentHashAlg?: SegmentIntegrityAlgorithm; segments: Segment[]; segmentSizeDefault?: number; encryptedSegmentSizeDefault?: number; @@ -153,10 +157,12 @@ export class SplitKey { }, integrityInformation: { rootSignature: { - alg: 'HS256', + // Placeholders; `writeStream` overwrites both once the payload has + // been segmented and the aggregate hash is known. + alg: ROOT_INTEGRITY_ALGORITHM, sig: '', }, - segmentHashAlg: 'GMAC', + segmentHashAlg: SEGMENT_INTEGRITY_ALGORITHM, segments: [], }, policy: policyForManifest, diff --git a/lib/tdf3/src/tdf.ts b/lib/tdf3/src/tdf.ts index 750ad0344..b22974d2d 100644 --- a/lib/tdf3/src/tdf.ts +++ b/lib/tdf3/src/tdf.ts @@ -149,7 +149,59 @@ type Chunk = { decryptedChunk: Mailbox; }; -export type IntegrityAlgorithm = 'GMAC' | 'HS256'; +/** + * Algorithms usable for *per-segment* integrity. + * + * `GMAC` is legitimate here: the segment's bytes were just processed by + * AES-GCM under the DEK, so the trailing 16 bytes are the tag the AEAD itself + * produced. + */ +export type SegmentIntegrityAlgorithm = 'GMAC' | 'HS256'; + +/** + * Algorithms usable for the *root* signature. + * + * Deliberately narrower than {@link SegmentIntegrityAlgorithm}: AES-GCM never + * processes the aggregate hash, so there is no tag to extract and `GMAC` would + * degenerate into copying the last segment hash — a keyless, forgeable value. + * The type carries the invariant so a root algorithm cannot even be *typed* as + * `'GMAC'`. + */ +export type RootIntegrityAlgorithm = 'HS256'; + +/** + * @deprecated Prefer {@link SegmentIntegrityAlgorithm} or + * {@link RootIntegrityAlgorithm}, which say which position they are valid in. + */ +export type IntegrityAlgorithm = SegmentIntegrityAlgorithm; + +/** The only root integrity algorithm this SDK writes. */ +export const ROOT_INTEGRITY_ALGORITHM: RootIntegrityAlgorithm = 'HS256'; + +/** Default per-segment integrity algorithm. */ +export const SEGMENT_INTEGRITY_ALGORITHM: SegmentIntegrityAlgorithm = 'GMAC'; + +/** Length in bytes of an AES-GCM authentication tag. */ +const GMAC_TAG_LENGTH = 16; + +/** + * Case-insensitive test for a supported segment integrity algorithm. + * An explicit allowlist, so unknown algorithms are rejected rather than + * silently defaulted. + */ +export function isSegmentIntegrityAlgorithm(alg: unknown): alg is SegmentIntegrityAlgorithm { + return typeof alg === 'string' && ['GMAC', 'HS256'].includes(alg.toUpperCase()); +} + +/** + * Case-insensitive test for a supported root integrity algorithm. + * + * `GMAC` is not a member in any casing. Only `HS256` produces a keyed MAC over + * the aggregate hash, so it is the only value this SDK will write. + */ +export function isRootIntegrityAlgorithm(alg: unknown): alg is RootIntegrityAlgorithm { + return typeof alg === 'string' && alg.toUpperCase() === ROOT_INTEGRITY_ALGORITHM; +} export type EncryptConfiguration = { allowList?: OriginAllowList; @@ -157,8 +209,8 @@ export type EncryptConfiguration = { dpopKeys: KeyPair; encryptionInformation: SplitKey; segmentSizeDefault: number; - integrityAlgorithm: IntegrityAlgorithm; - segmentIntegrityAlgorithm: IntegrityAlgorithm; + rootIntegrityAlgorithm: RootIntegrityAlgorithm; + segmentIntegrityAlgorithm: SegmentIntegrityAlgorithm; contentStream: ReadableStream; mimeType?: string; policy: Policy; @@ -362,7 +414,7 @@ async function getSignature( switch (algorithmType.toUpperCase()) { case 'GMAC': // use the auth tag baked into the encrypted payload - return content.slice(-16); + return content.slice(-GMAC_TAG_LENGTH); case 'HS256': { // Use CryptoService for HMAC-SHA256 signing return cryptoService.hmac(content, unwrappedKey); @@ -381,7 +433,10 @@ async function getSignatureVersion422( switch (algorithmType.toUpperCase()) { case 'GMAC': // use the auth tag baked into the encrypted payload - return buffToString(Uint8Array.from(payloadBinary.asByteArray()).slice(-16), 'hex'); + return buffToString( + Uint8Array.from(payloadBinary.asByteArray()).slice(-GMAC_TAG_LENGTH), + 'hex' + ); case 'HS256': { const content = buffToString(new Uint8Array(payloadBinary.asArrayBuffer()), 'utf-8'); const sig = await cryptoService.hmac(new TextEncoder().encode(content), unwrappedKey); @@ -403,6 +458,24 @@ export async function writeStream(cfg: EncryptConfiguration): Promise {}, + withCreds: async (httpReq: HttpRequest) => httpReq, +}; + +const SEGMENT_SIZE = 1024; +const SEGMENT_COUNT = 4; + +function segmentedPlaintext(count = SEGMENT_COUNT): Uint8Array { + const out = new Uint8Array(count * SEGMENT_SIZE); + for (let i = 0; i < count; i++) { + out.fill('A'.charCodeAt(0) + i, i * SEGMENT_SIZE, (i + 1) * SEGMENT_SIZE); + } + return out; +} + +function newClient(): Client.Client { + return new Client.Client({ + kasEndpoint: kasUrl, + platformUrl: kasUrl, + dpopKeys: Mocks.entityKeyPair(), + clientId: 'id', + authProvider, + }); +} + +type EncryptOverrides = Omit, 'source' | 'keyMiddleware'>; + +async function encryptToBuffer( + client: Client.Client, + plaintext: Uint8Array, + overrides: EncryptOverrides = {} +): Promise<{ buffer: Uint8Array; manifest: Manifest }> { + const encryptionInformation = new SplitKey(new AesGcmCipher(WebCryptoService)); + const key = await encryptionInformation.generateKey(); + const stream = await client.encrypt({ + metadata: Mocks.getMetadataObject(), + offline: true, + scope: { dissem: ['user@domain.com'], attributes: [] }, + windowSize: SEGMENT_SIZE, + keyMiddleware: async () => ({ keyForEncryption: key, keyForManifest: key }), + source: new ReadableStream({ + start(controller) { + controller.enqueue(plaintext); + controller.close(); + }, + }), + ...overrides, + }); + const buffer = await stream.toBuffer(); + return { buffer, manifest: stream.manifest }; +} + +function integrityInfo(manifest: Manifest) { + return manifest.encryptionInformation.integrityInformation; +} + +describe('integrity algorithm selection (DSPX-4736)', function () { + let client: Client.Client; + const plaintext = segmentedPlaintext(); + + beforeEach(function () { + client = newClient(); + }); + + it('defaults to an HS256 root and GMAC segments', async function () { + const { manifest } = await encryptToBuffer(client, plaintext); + const info = integrityInfo(manifest); + assert.equal(info.rootSignature.alg, 'HS256'); + assert.equal(info.segmentHashAlg, 'GMAC'); + }); + + it('accepts an explicit HS256 root and round-trips', async function () { + const { buffer, manifest } = await encryptToBuffer(client, plaintext, { + rootIntegrityAlgorithm: 'HS256', + }); + assert.equal(integrityInfo(manifest).rootSignature.alg, 'HS256'); + const stream = await client.decrypt({ source: { type: 'buffer', location: buffer } }); + assert.deepEqual(new Uint8Array(await stream.toBuffer()), plaintext); + }); + + for (const segmentIntegrityAlgorithm of ['GMAC', 'HS256'] as const) { + it(`accepts ${segmentIntegrityAlgorithm} segments and round-trips`, async function () { + const { buffer, manifest } = await encryptToBuffer(client, plaintext, { + segmentIntegrityAlgorithm, + }); + assert.equal(integrityInfo(manifest).segmentHashAlg, segmentIntegrityAlgorithm); + const stream = await client.decrypt({ source: { type: 'buffer', location: buffer } }); + assert.deepEqual(new Uint8Array(await stream.toBuffer()), plaintext); + }); + } + + it('refuses to write a GMAC root', async function () { + try { + await encryptToBuffer(client, plaintext, { + // Only reachable by casting: `RootIntegrityAlgorithm` cannot be 'GMAC'. + rootIntegrityAlgorithm: 'GMAC' as never, + }); + assert.fail('expected a ConfigurationError'); + } catch (e) { + assert.instanceOf(e, ConfigurationError); + assert.include((e as Error).message, 'unsupported root integrity algorithm'); + } + }); + + it('refuses to write an unknown root algorithm', async function () { + try { + await encryptToBuffer(client, plaintext, { + rootIntegrityAlgorithm: 'CRC32' as never, + }); + assert.fail('expected a ConfigurationError'); + } catch (e) { + assert.instanceOf(e, ConfigurationError); + assert.include((e as Error).message, 'unsupported root integrity algorithm'); + } + }); + + it('refuses to write an unknown segment algorithm', async function () { + try { + await encryptToBuffer(client, plaintext, { + segmentIntegrityAlgorithm: 'CRC32' as never, + }); + assert.fail('expected a ConfigurationError'); + } catch (e) { + assert.instanceOf(e, ConfigurationError); + assert.include((e as Error).message, 'unsupported segment integrity algorithm'); + } + }); + + // The guards accept any casing so that callers can hand us user input directly. + // What lands in the manifest is a separate question: readers -- ours included -- + // match the spec's uppercase spelling exactly, so a lowercase manifest is one no + // one can open. These pin the writer's canonicalization rather than the guards'. + for (const segment of ['gmac', 'hs256'] as const) { + it(`canonicalizes lowercase '${segment}' segments and a lowercase root`, async function () { + const { buffer, manifest } = await encryptToBuffer(client, plaintext, { + rootIntegrityAlgorithm: 'hs256' as never, + segmentIntegrityAlgorithm: segment as never, + }); + assert.equal(integrityInfo(manifest).rootSignature.alg, 'HS256'); + assert.equal(integrityInfo(manifest).segmentHashAlg, segment.toUpperCase()); + + // Re-read the bytes we actually wrote, not the in-memory manifest. + const { manifest: onDisk } = await client.loadTDFStream({ + source: { type: 'buffer', location: buffer }, + }); + assert.equal(integrityInfo(onDisk).rootSignature.alg, 'HS256'); + assert.equal(integrityInfo(onDisk).segmentHashAlg, segment.toUpperCase()); + + // And the strict reader can open it, which a lowercase manifest cannot. + const stream = await client.decrypt({ source: { type: 'buffer', location: buffer } }); + assert.deepEqual(new Uint8Array(await stream.toBuffer()), plaintext); + }); + } + + it('canonicalizes lowercase algorithms in a 4.2.2 manifest too', async function () { + const { manifest } = await encryptToBuffer(client, plaintext, { + tdfSpecVersion: '4.2.2', + rootIntegrityAlgorithm: 'hs256' as never, + segmentIntegrityAlgorithm: 'gmac' as never, + }); + assert.equal(integrityInfo(manifest).rootSignature.alg, 'HS256'); + assert.equal(integrityInfo(manifest).segmentHashAlg, 'GMAC'); + }); +});