Skip to content

Commit

Permalink
feat(sdk): ec-wrapped key support
Browse files Browse the repository at this point in the history
- Adds parameters for requesting EC encapsulation in TDF key access objects to SDK and CLI
  - new `createZTDF` parameter, `wrappingKeyAlgorithm`. Defaults to `"rsa:2048"`, and can now take `"ec:secp256r1"`.
  - cli `encrypt` exposes this as the `--encapKeyType` parameter
- Similarly, adds parameters for requesting responses with EC wrapped return values from key access server during decrypt
  - new `sdk.read` parameter, `wrappingKeyAlgorithm`. Defaults to `"rsa:2048"`, and can now take `"ec:secp256r1"`. Only used for ZTDF kaos currently.
  - exposes this through cli `decrypt` subcommand with the `--rewrapKeyType` parameter
  • Loading branch information
dmihalcik-virtru committed Feb 13, 2025
1 parent dfb1a6f commit de14435
Show file tree
Hide file tree
Showing 14 changed files with 534 additions and 266 deletions.
27 changes: 27 additions & 0 deletions cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
tdfSpecVersion,
OpenTDF,
DecoratedStream,
isPublicKeyAlgorithm,
} from '@opentdf/sdk';
import { CLIError, Level, log } from './logger.js';
import { webcrypto } from 'crypto';
Expand Down Expand Up @@ -174,6 +175,12 @@ async function parseReadOptions(argv: Partial<mainArgs>): Promise<ReadOptions> {
argv.assertionVerificationKeys
);
}
if (argv.rewrapKeyType?.length) {
if (!isPublicKeyAlgorithm(argv.rewrapKeyType)) {
throw new CLIError('CRITICAL', `Unsupported rewrap key algorithm: [${argv.rewrapKeyType}]`);
}
r.wrappingKeyAlgorithm = argv.rewrapKeyType;
}
if (argv.concurrencyLimit) {
r.concurrencyLimit = argv.concurrencyLimit;
} else {
Expand Down Expand Up @@ -269,6 +276,12 @@ async function parseCreateZTDFOptions(argv: Partial<mainArgs>): Promise<CreateZT
if (argv.assertions?.length) {
c.assertionConfigs = await parseAssertionConfig(argv.assertions);
}
if (argv.encapKeyType?.length) {
if (!isPublicKeyAlgorithm(argv.encapKeyType)) {
throw new CLIError('CRITICAL', `Unsupported rewrap key algorithm: [${argv.encapKeyType}]`);
}
c.wrappingKeyAlgorithm = argv.encapKeyType;
}
if (argv.mimeType?.length) {
if (argv.mimeType && /^[a-z]+\/[a-z0-9-+.]+$/.test(argv.mimeType)) {
c.mimeType = argv.mimeType as `${string}/${string}`;
Expand Down Expand Up @@ -449,6 +462,13 @@ export const handleArgs = (args: string[]) => {
description: 'Container format',
default: 'nano',
},
encapKeyType: {
alias: 'encapsulation-algorithm',
group: 'Encrypt Options:',
desc: 'Key type for wrapping keys',
type: 'string',
default: 'rsa:2048',
},
policyBinding: {
group: 'Encrypt Options:',
choices: bindingTypes,
Expand All @@ -461,6 +481,13 @@ export const handleArgs = (args: string[]) => {
type: 'string',
default: '',
},
rewrapKeyType: {
alias: 'rewrap-encapsulation-algorithm',
group: 'Decrypt Options:',
desc: 'Key type for rewrap',
type: 'string',
default: 'rsa:2048',
},
userId: {
group: 'Encrypt Options:',
type: 'string',
Expand Down
29 changes: 28 additions & 1 deletion lib/src/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,33 @@ export async function fetchWrappedKey(

export type KasPublicKeyAlgorithm = 'ec:secp256r1' | 'rsa:2048';

export const isPublicKeyAlgorithm = (a: string): a is KasPublicKeyAlgorithm => {
return a === 'ec:secp256r1' || a === 'rsa:2048';
};

export const keyAlgorithmToPublicKeyAlgorithm = (a: KeyAlgorithm): KasPublicKeyAlgorithm => {
if (a.name === 'ECDSA' || a.name === 'ECDH') {
const eca = a as EcKeyAlgorithm;
if (eca.namedCurve === 'P-256') {
return 'ec:secp256r1';
}
throw new Error(`unsupported EC curve: ${eca.namedCurve}`);
}
if (a.name === 'RSA-OAEP') {
const rsaa = a as RsaHashedKeyAlgorithm;
if (rsaa.modulusLength === 2048) {
// if (rsaa.hash.name !== 'RSASSA-PKCS1-v1_5') {
// throw new Error(`unsupported RSA hash: ${rsaa.hash.name}`);
// }
if (rsaa.publicExponent.toString() !== '1,0,1') {
throw new Error(`unsupported RSA public exponent: ${rsaa.publicExponent}`);
}
return 'rsa:2048';
}
}
throw new Error(`unsupported key algorithm: ${a.name}`);
};

/**
* Information about one of a KAS's published public keys.
* A KAS may publish multiple keys with a given algorithm type.
Expand Down Expand Up @@ -188,7 +215,7 @@ export async function fetchKasPubKey(
key: noteInvalidPublicKey(pkUrlV2, pemToCryptoPublicKey(publicKey)),
publicKey,
url: kasEndpoint,
algorithm: 'ec:secp256r1',
algorithm: algorithm || 'rsa:2048',
...(kid && { kid }),
};
}
Expand Down
2 changes: 1 addition & 1 deletion lib/src/nanotdf-crypto/generateKeyPair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface GenerateKeyPairOptions {
isExtractable: boolean;
}

export default async function generateKeyPair(
export async function generateKeyPair(
{ type: name, curve: namedCurve, keyUsages, isExtractable }: GenerateKeyPairOptions = {
type: AlgorithmName.ECDH,
curve: NamedCurve.P256,
Expand Down
2 changes: 1 addition & 1 deletion lib/src/nanotdf-crypto/generateRandomNumber.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Generate a random number of given length
*/
export default function generateRandomNumber(length: number): Uint8Array {
export function generateRandomNumber(length: number): Uint8Array {
const byteArray = new Uint8Array(length);
crypto.getRandomValues(byteArray);
return byteArray;
Expand Down
19 changes: 0 additions & 19 deletions lib/src/nanotdf-crypto/importRawKey.ts

This file was deleted.

5 changes: 2 additions & 3 deletions lib/src/nanotdf-crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ export { Ciphers } from './ciphers.js';
export { default as decrypt } from './decrypt.js';
export { default as digest } from './digest.js';
export { default as encrypt } from './encrypt.js';
export { default as generateKeyPair } from './generateKeyPair.js';
export { default as importRawKey } from './importRawKey.js';
export { generateKeyPair } from './generateKeyPair.js';
export { keyAgreement } from './keyAgreement.js';
export { default as exportCryptoKey } from './exportCryptoKey.js';
export { default as generateRandomNumber } from './generateRandomNumber.js';
export { generateRandomNumber } from './generateRandomNumber.js';
export { pemPublicToCrypto, pemCertToCrypto } from './pemPublicToCrypto.js';
export * as enums from './enums.js';
21 changes: 14 additions & 7 deletions lib/src/nanotdf-crypto/keyAgreement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

import { keyAlgorithmToPublicKeyAlgorithm } from '../access.js';
import { ConfigurationError } from '../errors.js';
import { AlgorithmName, CipherType, HashType, KeyFormat, KeyType, KeyUsageType } from './enums.js';

Expand Down Expand Up @@ -69,19 +70,25 @@ export async function keyAgreement(
isExtractable: true,
}
): Promise<CryptoKey> {
if (
publicKey?.algorithm?.name !== AlgorithmName.ECDSA &&
publicKey?.algorithm?.name !== AlgorithmName.ECDH
) {
throw new ConfigurationError('CryptoKey is expected to be of type ECDSA or ECDH');
for (const k of [privateKey, publicKey]) {
const mechanism = keyAlgorithmToPublicKeyAlgorithm(k.algorithm);
if (mechanism !== 'ec:secp256r1') {
throw new ConfigurationError(
`${k.type} CryptoKey is expected to be of type ECDSA or ECDH, not [${k.algorithm?.name}]`
);
}
}

if (privateKey.type !== KeyType.Private) {
throw new ConfigurationError('Expected input of privateKey to be a CryptoKey of type private');
throw new ConfigurationError(
`Expected input of privateKey to be a CryptoKey of type private, not [${privateKey.type}]`
);
}

if (publicKey.type !== KeyType.Public) {
throw new ConfigurationError('Expected input of publicKey to be a CryptoKey of type public');
throw new ConfigurationError(
`Expected input of publicKey to be a CryptoKey of type public, not [${publicKey.type}]`
);
}

const {
Expand Down
13 changes: 11 additions & 2 deletions lib/src/opentdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import Header from './nanotdf/models/Header.js';
import { fromSource, sourceToStream, type Source } from './seekable.js';
import { Client as TDF3Client } from '../tdf3/src/client/index.js';
import { AssertionConfig, AssertionVerificationKeys } from '../tdf3/src/assertions.js';
import { OriginAllowList } from './access.js';
import { type KasPublicKeyAlgorithm, OriginAllowList, isPublicKeyAlgorithm } from './access.js';
import { type Manifest } from '../tdf3/src/models/manifest.js';

export { type KasPublicKeyAlgorithm, isPublicKeyAlgorithm };

export type Keys = {
[keyID: string]: CryptoKey | CryptoKeyPair;
};
Expand Down Expand Up @@ -91,6 +93,9 @@ export type CreateZTDFOptions = CreateOptions & {
// The segment size for the content; smaller is slower, but allows faster random access.
// The current default is 1 MiB (2^20 bytes).
windowSize?: number;

// Preferred algorithm to use for Key Access Objects.
wrappingKeyAlgorithm?: KasPublicKeyAlgorithm;
};

// Settings for decrypting any variety of TDF file.
Expand All @@ -108,6 +113,9 @@ export type ReadOptions = {

// If set, prevents more than this number of concurrent requests to the KAS.
concurrencyLimit?: number;

// Type of key to use for wrapping responses.
wrappingKeyAlgorithm?: KasPublicKeyAlgorithm;
};

// Defaults and shared settings that are relevant to creating TDF objects.
Expand Down Expand Up @@ -138,7 +146,7 @@ export type OpenTDFOptions = {

export type DecoratedStream = ReadableStream<Uint8Array> & {
// If the source is a TDF3/ZTDF, and includes metadata, and it has been read.
metadata?: Promise<any>;
metadata?: Promise<unknown>;
manifest?: Promise<Manifest>;
// If the source is a NanoTDF, this will be set.
header?: Header;
Expand Down Expand Up @@ -323,6 +331,7 @@ export class OpenTDF {
allowList,
assertionVerificationKeys: opts.assertionVerificationKeys,
noVerifyAssertions: opts.noVerify,
wrappingKeyAlgorithm: opts.wrappingKeyAlgorithm,
});
const stream: DecoratedStream = oldStream.stream;
stream.metadata = Promise.resolve(oldStream.metadata);
Expand Down
6 changes: 5 additions & 1 deletion lib/tdf3/src/client/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { DecoratedReadableStream } from './DecoratedReadableStream.js';
import { type Chunker } from '../../../src/seekable.js';
import { AssertionConfig, AssertionVerificationKeys } from '../assertions.js';
import { Value } from '../../../src/policy/attributes.js';
import { OriginAllowList } from '../../../src/access.js';
import { KasPublicKeyAlgorithm, OriginAllowList } from '../../../src/access.js';

export const DEFAULT_SEGMENT_SIZE: number = 1024 * 1024;
export type Scope = {
Expand Down Expand Up @@ -52,6 +52,9 @@ export type EncryptParams = {
assertionConfigs?: AssertionConfig[];
defaultKASEndpoint?: string;

// Preferred wrapping key algorithm. Used when KID resolution is not available.
wrappingKeyAlgorithm?: KasPublicKeyAlgorithm;

// Unsupported
asHtml?: boolean;
// Unsupported
Expand Down Expand Up @@ -509,6 +512,7 @@ export type DecryptParams = {
assertionVerificationKeys?: AssertionVerificationKeys;
concurrencyLimit?: number;
noVerifyAssertions?: boolean;
wrappingKeyAlgorithm?: KasPublicKeyAlgorithm;
};

/**
Expand Down
Loading

0 comments on commit de14435

Please sign in to comment.