Skip to content

Commit 2f2cd63

Browse files
feat(sdk): require kid for ML-KEM key access (DSPX-3229)
ML-KEM wrapped key access objects must carry a kid so the KAS can identify which encapsulation key to unwrap with; unlike RSA/EC there is no way to recover the key without it. Previously MlKemWrapped accepted an optional kid and only emitted it when truthy, silently producing an unusable KAO. - MlKemWrapped now requires kid: string, rejects missing/blank values with a ConfigurationError, and always emits kid in the KAO. - buildKeyAccess guards the ML-KEM branch so callers get a clear ConfigurationError before constructing MlKemWrapped. - RSA (Wrapped) and EC (ECWrapped) keep kid optional, unchanged. Also derive the CLI key-type choices from the SDK algorithm list so the allowed set is declared once: - access.ts declares PUBLIC_KEY_ALGORITHMS as the single source of truth and derives KasPublicKeyAlgorithm and isPublicKeyAlgorithm from it. - opentdf.ts re-exports PUBLIC_KEY_ALGORITHMS from @opentdf/sdk. - cli.ts uses it as yargs choices for --encap-key-type and --rewrap-key-type, so invalid algorithms are rejected at parse time. Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
1 parent 819fe70 commit 2f2cd63

5 files changed

Lines changed: 30 additions & 25 deletions

File tree

cli/src/cli.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
OpenTDF,
1717
DecoratedStream,
1818
isPublicKeyAlgorithm,
19+
PUBLIC_KEY_ALGORITHMS,
1920
} from '@opentdf/sdk';
2021
import { CLIError, Level, log } from './logger.js';
2122
import * as assertions from '@opentdf/sdk/assertions';
@@ -462,6 +463,7 @@ export const handleArgs = (args: string[]) => {
462463
group: 'Encrypt Options:',
463464
desc: 'Key type for wrapping keys',
464465
type: 'string',
466+
choices: PUBLIC_KEY_ALGORITHMS,
465467
default: 'rsa:2048',
466468
},
467469
mimeType: {
@@ -475,6 +477,7 @@ export const handleArgs = (args: string[]) => {
475477
group: 'Decrypt Options:',
476478
desc: 'Key type for rewrap',
477479
type: 'string',
480+
choices: PUBLIC_KEY_ALGORITHMS,
478481
default: 'rsa:2048',
479482
},
480483
userId: {

lib/src/access.ts

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -87,28 +87,21 @@ export const rewrapAdditionalContextHeader = (
8787
return base64.encode(JSON.stringify(context));
8888
};
8989

90-
export type KasPublicKeyAlgorithm =
91-
| 'ec:secp256r1'
92-
| 'ec:secp384r1'
93-
| 'ec:secp521r1'
94-
| 'rsa:2048'
95-
| 'rsa:4096'
96-
| 'mlkem:512'
97-
| 'mlkem:768'
98-
| 'mlkem:1024';
90+
export const PUBLIC_KEY_ALGORITHMS = [
91+
'ec:secp256r1',
92+
'ec:secp384r1',
93+
'ec:secp521r1',
94+
'rsa:2048',
95+
'rsa:4096',
96+
'mlkem:512',
97+
'mlkem:768',
98+
'mlkem:1024',
99+
] as const;
99100

100-
export const isPublicKeyAlgorithm = (a: string): a is KasPublicKeyAlgorithm => {
101-
return (
102-
a === 'ec:secp256r1' ||
103-
a === 'ec:secp384r1' ||
104-
a === 'ec:secp521r1' ||
105-
a === 'rsa:2048' ||
106-
a === 'rsa:4096' ||
107-
a === 'mlkem:512' ||
108-
a === 'mlkem:768' ||
109-
a === 'mlkem:1024'
110-
);
111-
};
101+
export type KasPublicKeyAlgorithm = (typeof PUBLIC_KEY_ALGORITHMS)[number];
102+
103+
export const isPublicKeyAlgorithm = (a: string): a is KasPublicKeyAlgorithm =>
104+
(PUBLIC_KEY_ALGORITHMS as readonly string[]).includes(a);
112105

113106
export const keyAlgorithmToPublicKeyAlgorithm = (k: CryptoKey): KasPublicKeyAlgorithm => {
114107
const a = k.algorithm;

lib/src/opentdf.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import {
1515
type KasPublicKeyAlgorithm,
1616
OriginAllowList,
17+
PUBLIC_KEY_ALGORITHMS,
1718
fetchKeyAccessServers,
1819
isPublicKeyAlgorithm,
1920
} from './access.js';
@@ -45,6 +46,7 @@ export {
4546
type Payload,
4647
type Segment,
4748
type SplitType,
49+
PUBLIC_KEY_ALGORITHMS,
4850
isPublicKeyAlgorithm,
4951
};
5052

lib/tdf3/src/models/key-access.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { base64, hex } from '../../../src/encodings/index.js';
2+
import { ConfigurationError } from '../../../src/errors.js';
23
import { Binary } from '../binary.js';
34
import type { CryptoService, KeyPair, SymmetricKey } from '../crypto/declarations.js';
45
import { getZtdfSalt } from '../crypto/salt.js';
@@ -160,13 +161,16 @@ export class MlKemWrapped {
160161

161162
constructor(
162163
public readonly url: string,
163-
public readonly kid: string | undefined,
164+
public readonly kid: string,
164165
public readonly publicKey: string,
165166
public readonly metadata: unknown,
166167
public readonly cryptoService: CryptoService,
167168
public readonly sid: string | undefined,
168169
public readonly alg: 'mlkem:512' | 'mlkem:768' | 'mlkem:1024'
169170
) {
171+
if (!kid?.trim()) {
172+
throw new ConfigurationError('MlKemWrapped requires a non-empty kid');
173+
}
170174
this.level = parseInt(alg.split(':')[1], 10) as 512 | 768 | 1024;
171175
}
172176

@@ -230,9 +234,7 @@ export class MlKemWrapped {
230234
},
231235
schemaVersion,
232236
};
233-
if (this.kid) {
234-
kao.kid = this.kid;
235-
}
237+
kao.kid = this.kid;
236238
if (this.sid?.length) {
237239
kao.sid = this.sid;
238240
}

lib/tdf3/src/tdf.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,11 @@ export async function buildKeyAccess({
283283
switch (type) {
284284
case 'wrapped':
285285
if (alg === 'mlkem:512' || alg === 'mlkem:768' || alg === 'mlkem:1024') {
286+
if (!kid?.trim()) {
287+
throw new ConfigurationError(
288+
`buildKeyAccess: kid is required for ML-KEM algorithm [${alg}]`
289+
);
290+
}
286291
return new MlKemWrapped(url, kid, pubKey, metadata, cryptoService, sid, alg);
287292
}
288293
return new Wrapped(url, kid, pubKey, metadata, cryptoService, sid);

0 commit comments

Comments
 (0)