Skip to content

feat(sdk): ec-wrapped key support #422

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Feb 19, 2025
29 changes: 29 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 @@ -272,13 +279,20 @@ 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}`;
} else {
throw new CLIError('CRITICAL', 'Invalid mimeType format');
}
}
log('DEBUG', `CreateZTDFOptions: ${JSON.stringify(c)}`);
return c;
}

Expand All @@ -290,6 +304,7 @@ async function parseCreateNanoTDFOptions(argv: Partial<mainArgs>): Promise<Creat
}
// NOTE autoconfigure is not yet supported in nanotdf
delete c.autoconfigure;
log('DEBUG', `CreateNanoTDFOptions: ${JSON.stringify(c)}`);
return c;
}

Expand Down Expand Up @@ -452,6 +467,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 @@ -464,6 +486,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
38 changes: 37 additions & 1 deletion lib/src/access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,42 @@ 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}`);
};

export const publicKeyAlgorithmToJwa = (a: KasPublicKeyAlgorithm): string => {
switch (a) {
case 'ec:secp256r1':
return 'ES256';
case 'rsa:2048':
return 'RS256';
}
};

/**
* 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 +224,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
14 changes: 12 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 @@ -300,6 +308,7 @@ export class OpenTDF {
},
splitPlan: opts.splitPlan,
windowSize: opts.windowSize,
wrappingKeyAlgorithm: opts.wrappingKeyAlgorithm,
});
const stream: DecoratedStream = oldStream.stream;
stream.manifest = Promise.resolve(oldStream.manifest);
Expand All @@ -323,6 +332,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 @@ -517,6 +520,7 @@ export type DecryptParams = {
assertionVerificationKeys?: AssertionVerificationKeys;
concurrencyLimit?: number;
noVerifyAssertions?: boolean;
wrappingKeyAlgorithm?: KasPublicKeyAlgorithm;
};

/**
Expand Down
Loading
Loading