diff --git a/lib/src/access.ts b/lib/src/access.ts index cbffbbb6..4b311140 100644 --- a/lib/src/access.ts +++ b/lib/src/access.ts @@ -114,6 +114,15 @@ export const keyAlgorithmToPublicKeyAlgorithm = (a: KeyAlgorithm): KasPublicKeyA 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. diff --git a/lib/tdf3/src/models/key-access.ts b/lib/tdf3/src/models/key-access.ts index e7587146..b43a5a54 100644 --- a/lib/tdf3/src/models/key-access.ts +++ b/lib/tdf3/src/models/key-access.ts @@ -1,5 +1,4 @@ import { base64, hex } from '../../../src/encodings/index.js'; -import { generateKeyPair } from '../../../src/nanotdf-crypto/generateKeyPair.js'; import { generateRandomNumber } from '../../../src/nanotdf-crypto/generateRandomNumber.js'; import { keyAgreement } from '../../../src/nanotdf-crypto/keyAgreement.js'; import { pemPublicToCrypto } from '../../../src/nanotdf-crypto/pemPublicToCrypto.js'; @@ -18,7 +17,7 @@ export function isRemote(keyAccessJSON: KeyAccess | KeyAccessObject): boolean { export class ECWrapped { readonly type = 'ec-wrapped'; - readonly ephemeralKeyPair; + readonly ephemeralKeyPair: Promise; keyAccessObject?: KeyAccessObject; constructor( @@ -28,7 +27,14 @@ export class ECWrapped { public readonly metadata: unknown, public readonly sid: string ) { - this.ephemeralKeyPair = generateKeyPair(); + this.ephemeralKeyPair = crypto.subtle.generateKey( + { + name: 'ECDH', + namedCurve: 'P-256', + }, + false, + ['deriveBits', 'deriveKey'] + ); } async write( diff --git a/lib/tdf3/src/tdf.ts b/lib/tdf3/src/tdf.ts index f0e4bd10..b0583247 100644 --- a/lib/tdf3/src/tdf.ts +++ b/lib/tdf3/src/tdf.ts @@ -6,6 +6,7 @@ import { OriginAllowList, fetchKasPubKey as fetchKasPubKeyV2, fetchWrappedKey, + publicKeyAlgorithmToJwa, } from '../../src/access.js'; import { type AuthProvider, reqSignature } from '../../src/auth/auth.js'; import { allPool, anyPool } from '../../src/concurrency.js'; @@ -80,6 +81,7 @@ export type Metadata = { export type BuildKeyAccess = { type: KeyAccessType; + alg?: KasPublicKeyAlgorithm; url?: string; kid?: string; publicKey: string; @@ -201,13 +203,17 @@ export async function fetchKasPublicKey( return fetchKasPubKeyV2(kas, algorithm || 'rsa:2048'); } -export async function extractPemFromKeyString(keyString: string): Promise { +export async function extractPemFromKeyString( + keyString: string, + alg: KasPublicKeyAlgorithm +): Promise { let pem: string = keyString; // Skip the public key extraction if we find that the KAS url provides a // PEM-encoded key instead of certificate if (keyString.includes('CERTIFICATE')) { - const cert = await importX509(keyString, 'RS256', { extractable: true }); + const a = publicKeyAlgorithmToJwa(alg); + const cert = await importX509(keyString, a, { extractable: true }); pem = await exportSPKI(cert); } @@ -234,32 +240,34 @@ export async function buildKeyAccess({ kid, metadata, sid = '', + alg = 'rsa:2048', }: BuildKeyAccess): Promise { - /** Internal function to keep it DRY */ - function createKeyAccess( - type: KeyAccessType, - kasUrl: string, - kasKeyIdentifier: string | undefined, - pubKey: string, - metadata?: Metadata - ) { - switch (type) { - case 'wrapped': - return new Wrapped(kasUrl, kasKeyIdentifier, pubKey, metadata, sid); - case 'ec-wrapped': - return new ECWrapped(kasUrl, kasKeyIdentifier, pubKey, metadata, sid); - default: - throw new ConfigurationError(`buildKeyAccess: Key access type [${type}] is unsupported`); - } - } - // if url and pulicKey are specified load the key access object with them - if (url && publicKey) { - return createKeyAccess(type, url, kid, await extractPemFromKeyString(publicKey), metadata); + if (!url && !publicKey) { + throw new ConfigurationError('TDF.buildKeyAccess: No source for kasUrl or pubKey'); + } else if (!url) { + throw new ConfigurationError('TDF.buildKeyAccess: No kasUrl'); + } else if (!publicKey) { + throw new ConfigurationError('TDF.buildKeyAccess: No kas public key'); } - // All failed. Raise an error. - throw new ConfigurationError('TDF.buildKeyAccess: No source for kasUrl or pubKey'); + let pubKey: string; + try { + pubKey = await extractPemFromKeyString(publicKey, alg); + } catch (e) { + throw new ConfigurationError( + `TDF.buildKeyAccess: Invalid public key [${publicKey}], caused by [${e}]`, + e + ); + } + switch (type) { + case 'wrapped': + return new Wrapped(url, kid, pubKey, metadata, sid); + case 'ec-wrapped': + return new ECWrapped(url, kid, pubKey, metadata, sid); + default: + throw new ConfigurationError(`buildKeyAccess: Key access type [${type}] is unsupported`); + } } export function validatePolicyObject(policy: Policy): void { diff --git a/lib/tests/mocha/encrypt-decrypt.spec.ts b/lib/tests/mocha/encrypt-decrypt.spec.ts index 0968a4aa..cff2c1d0 100644 --- a/lib/tests/mocha/encrypt-decrypt.spec.ts +++ b/lib/tests/mocha/encrypt-decrypt.spec.ts @@ -278,6 +278,7 @@ describe('encrypt decrypt test', async function () { authProvider, }); const keyPair = await crypto.subtle.generateKey( + const assertionKeys = await crypto.subtle.generateKey( { name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, @@ -287,7 +288,7 @@ describe('encrypt decrypt test', async function () { true, ['sign', 'verify'] ); - const publicKey = keyPair.publicKey; + const assertionPublicKey = assertionKeys.publicKey; const scope: Scope = { dissem: ['user@domain.com'], attributes: [], @@ -297,6 +298,8 @@ describe('encrypt decrypt test', async function () { const hs256Key = new Uint8Array(32); crypto.getRandomValues(hs256Key); + console.log('ASDF about to encrypt'); + const encryptedStream = await client.encrypt({ metadata: Mocks.getMetadataObject(), wrappingKeyAlgorithm: encapKeyType, @@ -337,7 +340,7 @@ describe('encrypt decrypt test', async function () { appliesToState: 'encrypted', signingKey: { alg: 'RS256', - key: keyPair.privateKey, + key: assertionKeys.privateKey, }, }, { @@ -364,7 +367,7 @@ describe('encrypt decrypt test', async function () { }, assertion2: { alg: 'RS256', - key: publicKey, + key: assertionPublicKey, }, }, }; diff --git a/lib/tests/mocha/unit/tdf.spec.ts b/lib/tests/mocha/unit/tdf.spec.ts index 0767f5fb..b16bbf1d 100644 --- a/lib/tests/mocha/unit/tdf.spec.ts +++ b/lib/tests/mocha/unit/tdf.spec.ts @@ -5,6 +5,7 @@ import { KeyAccessObject } from '../../../tdf3/src/models/key-access.js'; import { PolicyBody, type Policy } from '../../../tdf3/src/models/policy.js'; import { OriginAllowList } from '../../../src/access.js'; import { ConfigurationError, InvalidFileError, UnsafeUrlError } from '../../../src/errors.js'; +import { getMocks } from '../../mocks/index.js'; const sampleCert = ` -----BEGIN CERTIFICATE----- @@ -42,9 +43,11 @@ HJg= -----END CERTIFICATE----- `.trim(); +const { kasECCert } = getMocks(); + describe('TDF', () => { it('should return key', async () => { - const pem = await TDF.extractPemFromKeyString(sampleCert); + const pem = await TDF.extractPemFromKeyString(sampleCert, 'rsa:2048'); expect(pem).to.include('-----BEGIN PUBLIC KEY-----'); expect(pem).to.include('-----END PUBLIC KEY-----'); }); @@ -53,9 +56,15 @@ describe('TDF', () => { const sampleKey = sampleCert .replace('BEGIN CERTIFICATE', 'BEGIN PUBLIC KEY') .replace('END CERTIFICATE', 'END PUBLIC KEY'); - const pem = await TDF.extractPemFromKeyString(sampleKey); + const pem = await TDF.extractPemFromKeyString(sampleKey, 'rsa:2048'); expect(pem).to.equal(sampleKey); }); + + it('should return ec pem', async () => { + const pem = await TDF.extractPemFromKeyString(kasECCert, 'ec:secp256r1'); + expect(pem).to.include('-----BEGIN PUBLIC KEY-----'); + expect(pem).to.include('-----END PUBLIC KEY-----'); + }); }); describe('fetchKasPublicKey', async () => { diff --git a/lib/tests/server.ts b/lib/tests/server.ts index 9e87c1ca..f05f5664 100644 --- a/lib/tests/server.ts +++ b/lib/tests/server.ts @@ -211,7 +211,7 @@ const kas: RequestListener = async (req, res) => { namedCurve: 'P-256', }, false, - ['deriveKey'] + ['deriveBits', 'deriveKey'] ); const kek = await keyAgreement(sessionKeyPair.privateKey, clientPublicKey, { hkdfSalt: new TextEncoder().encode('salt'),