Skip to content

Commit 5bde0a1

Browse files
authored
feat: get kas public key from base key (#623)
1 parent 35c9777 commit 5bde0a1

File tree

10 files changed

+196
-33
lines changed

10 files changed

+196
-33
lines changed

cli/package-lock.json

Lines changed: 16 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/src/access.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { ServiceError } from './errors.js';
33
import { RewrapResponse } from './platform/kas/kas_pb.js';
44
import { getPlatformUrlFromKasEndpoint, validateSecureUrl } from './utils.js';
55

6-
import { fetchKeyAccessServers as fetchKeyAccessServersRpc } from './access/access-rpc.js';
6+
import {
7+
fetchKasBasePubKey,
8+
fetchKeyAccessServers as fetchKeyAccessServersRpc,
9+
} from './access/access-rpc.js';
710
import { fetchKeyAccessServers as fetchKeyAccessServersLegacy } from './access/access-fetch.js';
811
import { fetchWrappedKey as fetchWrappedKeysRpc } from './access/access-rpc.js';
912
import { fetchWrappedKey as fetchWrappedKeysLegacy } from './access/access-fetch.js';
@@ -144,17 +147,34 @@ export async function fetchKeyAccessServers(
144147
}
145148

146149
/**
147-
* If we have KAS url but not public key we can fetch it from KAS, fetching
148-
* the value from `${kas}/kas_public_key`.
150+
* Fetch the EC (secp256r1) public key for a KAS endpoint.
151+
* @param kasEndpoint The KAS endpoint URL.
152+
* @returns The public key information for the KAS endpoint.
149153
*/
150154
export async function fetchECKasPubKey(kasEndpoint: string): Promise<KasPublicKeyInfo> {
151155
return fetchKasPubKey(kasEndpoint, 'ec:secp256r1');
152156
}
153157

158+
/**
159+
* Fetch the public key for a KAS endpoint.
160+
* This function will first try to fetch the base public key,
161+
* then it will try to fetch the public key using the RPC method,
162+
* and finally it will try to fetch the public key using the legacy method.
163+
* If all attempts fail, it will return the error from RPC Public Key fetch.
164+
* @param kasEndpoint The KAS endpoint URL.
165+
* @param algorithm Optional algorithm to fetch the public key for.
166+
* @returns The public key information.
167+
*/
154168
export async function fetchKasPubKey(
155169
kasEndpoint: string,
156170
algorithm?: KasPublicKeyAlgorithm
157171
): Promise<KasPublicKeyInfo> {
172+
try {
173+
return await fetchKasBasePubKey(kasEndpoint);
174+
} catch (e) {
175+
console.log(e);
176+
}
177+
158178
return await tryPromisesUntilFirstSuccess(
159179
() => fetchKasPubKeyRpc(kasEndpoint, algorithm),
160180
() => fetchKasPubKeyLegacy(kasEndpoint, algorithm)

lib/src/access/access-rpc.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
isPublicKeyAlgorithm,
23
KasPublicKeyAlgorithm,
34
KasPublicKeyInfo,
45
noteInvalidPublicKey,
@@ -74,6 +75,31 @@ export async function fetchKeyAccessServers(
7475
return new OriginAllowList(serverUrls, false);
7576
}
7677

78+
interface PlatformBaseKey {
79+
kas_id?: string;
80+
kas_uri: string;
81+
public_key: {
82+
algorithm: KasPublicKeyAlgorithm;
83+
kid: string;
84+
pem: string;
85+
};
86+
}
87+
88+
function isBaseKey(baseKey?: unknown): baseKey is PlatformBaseKey {
89+
if (!baseKey) {
90+
return false;
91+
}
92+
const bk = baseKey as PlatformBaseKey;
93+
return (
94+
!!bk.kas_uri &&
95+
!!bk.public_key &&
96+
typeof bk.public_key === 'object' &&
97+
!!bk.public_key.pem &&
98+
!!bk.public_key.algorithm &&
99+
isPublicKeyAlgorithm(bk.public_key.algorithm)
100+
);
101+
}
102+
77103
export async function fetchKasPubKey(
78104
kasEndpoint: string,
79105
algorithm?: KasPublicKeyAlgorithm
@@ -105,3 +131,45 @@ export async function fetchKasPubKey(
105131
throw new NetworkError(`[${platformUrl}] [PublicKey] ${extractRpcErrorMessage(e)}`);
106132
}
107133
}
134+
135+
/**
136+
* Fetch the base public key from WellKnownConfiguration of the platform.
137+
* @param kasEndpoint The KAS endpoint URL.
138+
* @throws {ConfigurationError} If the KAS endpoint is not defined.
139+
* @throws {NetworkError} If there is an error fetching the public key from the KAS endpoint.
140+
* @returns The base public key information for the KAS endpoint.
141+
*/
142+
export async function fetchKasBasePubKey(kasEndpoint: string): Promise<KasPublicKeyInfo> {
143+
if (!kasEndpoint) {
144+
throw new ConfigurationError('KAS definition not found');
145+
}
146+
validateSecureUrl(kasEndpoint);
147+
148+
const platformUrl = getPlatformUrlFromKasEndpoint(kasEndpoint);
149+
const platform = new PlatformClient({
150+
platformUrl,
151+
});
152+
try {
153+
const { configuration } = await platform.v1.wellknown.getWellKnownConfiguration({});
154+
const baseKey = configuration?.base_key as unknown as PlatformBaseKey;
155+
if (!isBaseKey(baseKey)) {
156+
throw new NetworkError(
157+
`Invalid Platform Configuration: [${kasEndpoint}] is missing BaseKey in WellKnownConfiguration`
158+
);
159+
}
160+
161+
const result: KasPublicKeyInfo = {
162+
key: noteInvalidPublicKey(
163+
new URL(baseKey.kas_uri),
164+
pemToCryptoPublicKey(baseKey.public_key.pem)
165+
),
166+
publicKey: baseKey.public_key.pem,
167+
url: baseKey.kas_uri,
168+
algorithm: baseKey.public_key.algorithm,
169+
kid: baseKey.public_key.kid,
170+
};
171+
return result;
172+
} catch (e) {
173+
throw new NetworkError(`[${platformUrl}] [PublicKey] ${extractRpcErrorMessage(e)}`);
174+
}
175+
}

lib/src/opentdf.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ export class OpenTDF {
335335
this.tdf3Client = new TDF3Client({
336336
authProvider,
337337
dpopKeys,
338-
kasEndpoint: 'https://disallow.all.invalid',
338+
kasEndpoint: this.platformUrl || 'https://disallow.all.invalid',
339339
policyEndpoint,
340340
});
341341
this.dpopKeys =
@@ -522,14 +522,17 @@ class NanoTDFReader {
522522
r.header = nanotdf.header;
523523
return r;
524524
}
525+
const platformUrl = this.opts.platformUrl || this.outer.platformUrl;
526+
const kasEndpoint =
527+
this.opts.allowedKASEndpoints?.[0] || platformUrl || 'https://disallow.all.invalid';
525528
const nc = new Client({
526529
allowedKases: this.opts.allowedKASEndpoints,
527530
authProvider: this.outer.authProvider,
528531
ignoreAllowList: this.opts.ignoreAllowlist,
529532
dpopEnabled: this.outer.dpopEnabled,
530533
dpopKeys: this.outer.dpopKeys,
531-
kasEndpoint: this.opts.allowedKASEndpoints?.[0] || 'https://disallow.all.invalid',
532-
platformUrl: this.opts.platformUrl || this.outer.platformUrl,
534+
kasEndpoint,
535+
platformUrl,
533536
});
534537
// TODO: The version number should be fetched from the API
535538
const version = '0.0.1';
@@ -691,9 +694,12 @@ class Collection {
691694
break;
692695
}
693696

697+
const kasEndpoint =
698+
opts.defaultKASEndpoint || opts.platformUrl || 'https://disallow.all.invalid';
699+
694700
this.client = new NanoTDFDatasetClient({
695701
authProvider,
696-
kasEndpoint: opts.defaultKASEndpoint ?? 'https://disallow.all.invalid',
702+
kasEndpoint: kasEndpoint,
697703
maxKeyIterations: opts.maxKeyIterations,
698704
platformUrl: opts.platformUrl,
699705
});

lib/tdf3/src/client/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ export class Client {
398398
keyMiddleware = defaultKeyMiddleware,
399399
streamMiddleware = async (stream: DecoratedReadableStream) => stream,
400400
tdfSpecVersion,
401-
wrappingKeyAlgorithm = 'rsa:2048',
401+
wrappingKeyAlgorithm,
402402
} = opts;
403403
const scope = opts.scope ?? { attributes: [], dissem: [] };
404404

@@ -462,7 +462,6 @@ export class Client {
462462
? maxByteLimit
463463
: opts.byteLimit;
464464
const encryptionInformation = new SplitKey(new AesGcmCipher(this.cryptoService));
465-
// TODO KAS: check here
466465
const splits: SplitStep[] = splitPlan?.length
467466
? splitPlan
468467
: [{ kas: opts.defaultKASEndpoint ?? this.kasEndpoint }];

lib/tdf3/src/tdf.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export async function fetchKasPublicKey(
199199
kas: string,
200200
algorithm?: KasPublicKeyAlgorithm
201201
): Promise<KasPublicKeyInfo> {
202-
return fetchKasPubKeyV2(kas, algorithm || 'rsa:2048');
202+
return fetchKasPubKeyV2(kas, algorithm);
203203
}
204204

205205
export async function extractPemFromKeyString(

lib/tests/mocha/unit/tdf.spec.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ describe('fetchKasPublicKey', async () => {
9191
it('localhost kas is valid', async () => {
9292
const pk2 = await TDF.fetchKasPublicKey('http://localhost:3000');
9393
expect(pk2.publicKey).to.include('BEGIN CERTIFICATE');
94-
expect(pk2.kid).to.equal('r1');
94+
expect(pk2.kid).to.equal('e1');
9595
});
9696

9797
it('invalid algorithms', async () => {
@@ -100,10 +100,15 @@ describe('fetchKasPublicKey', async () => {
100100
console.log(res);
101101
expect.fail('did not throw');
102102
} catch (e) {
103-
// TODO RPC: check why error thrown is failed to fetch
104103
expect(!!e).to.equal(true);
105104
}
106105
});
106+
107+
it('localhost BaseKey', async () => {
108+
const pk2 = await TDF.fetchKasPublicKey('http://localhost:3000');
109+
expect(pk2.publicKey).to.include('BEGIN CERTIFICATE');
110+
expect(pk2.kid).to.equal('e1');
111+
});
107112
});
108113

109114
describe('validatePolicyObject', () => {

lib/tests/server.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,21 @@ const kas: RequestListener = async (req, res) => {
421421
) {
422422
res.statusCode = 200;
423423
res.setHeader('Content-Type', 'application/json');
424-
res.end(JSON.stringify({ health: { endpoint: '/healthz' } }));
424+
res.end(
425+
JSON.stringify({
426+
configuration: {
427+
base_key: {
428+
kas_id: '34f2acdc-3d9c-4e92-80b6-90fe4dc9afcb',
429+
kas_uri: 'http://localhost:3000',
430+
public_key: {
431+
algorithm: 'ec:secp256r1',
432+
kid: 'e1',
433+
pem: Mocks.kasECCert,
434+
},
435+
},
436+
},
437+
})
438+
);
425439
return;
426440
} else if (url.pathname === '/policy.attributes.AttributesService/ListAttributes') {
427441
const token = req.headers['authorization'] as string;

0 commit comments

Comments
 (0)