Skip to content

Commit ec3d2c9

Browse files
committed
Test coverage for httpbis spec
1 parent 674aac3 commit ec3d2c9

25 files changed

+1883
-85
lines changed

src/algorithm/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from 'crypto';
1515
import { RSA_PKCS1_PADDING, RSA_PKCS1_PSS_PADDING } from 'constants';
1616
import { SigningKey, Algorithm, Verifier } from '../types';
17+
import { UnknownAlgorithmError } from '../errors';
1718

1819
/**
1920
* A helper method for easier consumption of the library.
@@ -59,7 +60,7 @@ export function createSigner(key: BinaryLike | KeyLike | SignKeyObjectInput | Si
5960
// signer.sign = async (data: Buffer) => createSign('ed25519').update(data).sign(key as KeyLike);
6061
break;
6162
default:
62-
throw new Error(`Unsupported signing algorithm ${alg}`);
63+
throw new UnknownAlgorithmError(`Unsupported signing algorithm ${alg}`);
6364
}
6465
if (id) {
6566
signer.id = id;
@@ -116,7 +117,7 @@ export function createVerifier(key: BinaryLike | KeyLike | VerifyKeyObjectInput
116117
verifier = async (data: Buffer, signature: Buffer) => verify(null, data, key as KeyLike, signature) as unknown as boolean;
117118
break;
118119
default:
119-
throw new Error(`Unsupported signing algorithm ${alg}`);
120+
throw new UnknownAlgorithmError(`Unsupported signing algorithm ${alg}`);
120121
}
121122
return Object.assign(verifier, { alg });
122123
}

src/errors/expired-error.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { VerificationError } from './verification-error';
2+
3+
export class ExpiredError extends VerificationError {
4+
}

src/errors/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1+
export { ExpiredError } from './expired-error';
2+
export { MalformedSignatureError } from './malformed-signature-error';
3+
export { UnacceptableSignatureError } from './unacceptable-signature-error';
4+
export { UnknownAlgorithmError } from './unknown-algorithm-error';
5+
export { UnknownKeyError } from './unknown-key-error';
16
export { UnsupportedAlgorithmError } from './unsupported-algorithm-error';
7+
export { VerificationError } from './verification-error';
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { VerificationError } from './verification-error';
2+
3+
export class MalformedSignatureError extends VerificationError {
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { VerificationError } from './verification-error';
2+
3+
export class UnacceptableSignatureError extends VerificationError {
4+
}

src/errors/unknown-algorithm-error.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* Thrown when a verifier/signer is created with an unknown algorithm
3+
*/
4+
export class UnknownAlgorithmError extends Error {
5+
}

src/errors/unknown-key-error.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { VerificationError } from './verification-error';
2+
3+
export class UnknownKeyError extends VerificationError {
4+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { VerificationError } from './verification-error';
2+
13
/**
24
* Thrown when a key is presented to verify a signature with
35
* an algorithm that is not supported
46
*/
5-
export class UnsupportedAlgorithmError extends Error {
7+
export class UnsupportedAlgorithmError extends VerificationError {
68
}

src/errors/verification-error.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export class VerificationError extends Error {
2+
}

src/httpbis/index.ts

Lines changed: 51 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,21 @@ import { Dictionary, parseHeader, quoteString } from '../structured-header';
1717
import {
1818
Request,
1919
Response,
20+
SignatureParameters,
2021
SignConfig,
2122
VerifyConfig,
2223
defaultParams,
2324
isRequest,
2425
CommonConfig,
2526
VerifyingKey,
2627
} from '../types';
27-
import { UnsupportedAlgorithmError } from '../errors';
28+
import {
29+
ExpiredError,
30+
MalformedSignatureError,
31+
UnacceptableSignatureError,
32+
UnknownKeyError,
33+
UnsupportedAlgorithmError,
34+
} from '../errors';
2835

2936
export function deriveComponent(component: string, params: Map<string, string | number | boolean>, res: Response, req?: Request): string[];
3037
export function deriveComponent(component: string, params: Map<string, string | number | boolean>, req: Request): string[];
@@ -49,7 +56,7 @@ export function deriveComponent(component: string, params: Map<string, string |
4956
return [context.method.toUpperCase()];
5057
case '@target-uri': {
5158
if (!isRequest(context)) {
52-
throw new Error('Cannot derive @target-url on response');
59+
throw new Error('Cannot derive @target-uri on response');
5360
}
5461
return [context.url.toString()];
5562
}
@@ -248,7 +255,7 @@ export function createSigningParameters(config: SignConfig): Parameters {
248255
}
249256
default:
250257
if (config.paramValues?.[paramName] instanceof Date) {
251-
value = Math.floor((config.paramValues[paramName] as Date).getTime() / 1000).toString();
258+
value = Math.floor((config.paramValues[paramName] as Date).getTime() / 1000);
252259
} else if (config.paramValues?.[paramName]) {
253260
value = config.paramValues[paramName] as string;
254261
}
@@ -287,7 +294,7 @@ export function augmentHeaders(headers: Record<string, string | string[]>, signa
287294
let signatureName = name ?? 'sig';
288295
if (signatureHeader.has(signatureName) || inputHeader.has(signatureName)) {
289296
let count = 0;
290-
while (signatureHeader?.has(`${signatureName}${count}`) || inputHeader?.has(`${signatureName}${count}`)) {
297+
while (signatureHeader.has(`${signatureName}${count}`) || inputHeader.has(`${signatureName}${count}`)) {
291298
count++;
292299
}
293300
signatureName += count.toString();
@@ -308,7 +315,7 @@ export async function signMessage<T extends Request = Request>(config: SignConfi
308315
export async function signMessage<T extends Request | Response = Request | Response, U extends Request = Request>(config: SignConfig, message: T, req?: U): Promise<T> {
309316
const signingParameters = createSigningParameters(config);
310317
const signatureBase = createSignatureBase({
311-
fields: config?.fields ?? [],
318+
fields: config.fields ?? [],
312319
componentParser: config.componentParser,
313320
}, message as Response, req);
314321
const signatureInput = serializeList([
@@ -323,7 +330,7 @@ export async function signMessage<T extends Request | Response = Request | Respo
323330
const signature = await config.key.sign(Buffer.from(base));
324331
return {
325332
...message,
326-
headers: augmentHeaders({...message.headers}, signature, signatureInput, config.name),
333+
headers: augmentHeaders({ ...message.headers }, signature, signatureInput, config.name),
327334
};
328335
}
329336

@@ -360,70 +367,68 @@ export async function verifyMessage(config: VerifyConfig, message: Request | Res
360367
const requiredParams = config.requiredParams ?? [];
361368
const requiredFields = config.requiredFields ?? [];
362369
return Array.from(signatureInputs.entries()).reduce<Promise<boolean | null>>(async (prev, [name, input]) => {
363-
const [result, key]: [Error | boolean | null, VerifyingKey] = await Promise.all([
370+
const signatureParams: SignatureParameters = Array.from(input[1].entries()).reduce((params, [key, value]) => {
371+
if (value instanceof ByteSequence) {
372+
Object.assign(params, {
373+
[key]: value.toBase64(),
374+
});
375+
} else if (value instanceof Token) {
376+
Object.assign(params, {
377+
[key]: value.toString(),
378+
});
379+
} else if (key === 'created' || key === 'expired') {
380+
Object.assign(params, {
381+
[key]: new Date((value as number) * 1000),
382+
});
383+
} else {
384+
Object.assign(params, {
385+
[key]: value,
386+
});
387+
}
388+
return params;
389+
}, {});
390+
const [result, key]: [Error | boolean | null, VerifyingKey | null] = await Promise.all([
364391
prev.catch((e) => e),
365-
config.keyLookup(Array.from(input[1].entries()).reduce((params, [key, value]) => {
366-
if (value instanceof ByteSequence) {
367-
Object.assign(params, {
368-
[key]: value.toBase64(),
369-
});
370-
} else if (value instanceof Token) {
371-
Object.assign(params, {
372-
[key]: value.toString(),
373-
});
374-
} else {
375-
Object.assign(params, {
376-
[key]: value,
377-
});
378-
}
379-
return params;
380-
}, {})),
392+
config.keyLookup(signatureParams),
381393
]);
382-
if (input[1].has('alg') && key.algs?.includes(input[1].get('alg') as string) === false) {
383-
throw new UnsupportedAlgorithmError('Unsupported key algorithm');
384-
}
385394
// @todo - confirm this is all working as expected
386-
if (!config.all && !key) {
387-
return null;
388-
}
389-
if (!config.all && result === true) {
390-
return result;
395+
if (config.all && !key) {
396+
throw new UnknownKeyError('Unknown key');
391397
}
392-
if (config.all && result !== true && result !== null) {
398+
if (!key) {
393399
if (result instanceof Error) {
394400
throw result;
395401
}
396402
return result;
397403
}
404+
if (input[1].has('alg') && key.algs?.includes(input[1].get('alg') as string) === false) {
405+
throw new UnsupportedAlgorithmError('Unsupported key algorithm');
406+
}
398407
if (!isInnerList(input)) {
399-
throw new Error('Malformed signature input');
408+
throw new MalformedSignatureError('Malformed signature input');
400409
}
401410
const hasRequiredParams = requiredParams.every((param) => input[1].has(param));
402411
if (!hasRequiredParams) {
403-
return false;
412+
throw new UnacceptableSignatureError('Missing required signature parameters');
404413
}
405414
// this could be tricky, what if we say "@method" but there is "@method;req"
406415
const hasRequiredFields = requiredFields.every((field) => input[0].some(([fieldName]) => fieldName === field));
407416
if (!hasRequiredFields) {
408-
return false;
417+
throw new UnacceptableSignatureError('Missing required signed fields');
409418
}
410419
if (input[1].has('created')) {
411420
const created = input[1].get('created') as number - tolerance;
412421
// maxAge overrides expires.
413422
// signature is older than maxAge
414-
if (maxAge && created - now > maxAge) {
415-
return false;
416-
}
417-
// created after the allowed time (ie: created in the future)
418-
if (created > notAfter) {
419-
return false;
423+
if ((maxAge && now - created > maxAge) || created > notAfter) {
424+
throw new ExpiredError('Signature is too old');
420425
}
421426
}
422427
if (input[1].has('expires')) {
423428
const expires = input[1].get('expires') as number + tolerance;
424429
// expired signature
425-
if (expires > now) {
426-
return false;
430+
if (now > expires) {
431+
throw new ExpiredError('Signature has expired');
427432
}
428433
}
429434

@@ -434,29 +439,11 @@ export async function verifyMessage(config: VerifyConfig, message: Request | Res
434439
const base = formatSignatureBase(signingBase);
435440
const signature = signatures.get(name);
436441
if (!signature) {
437-
throw new Error('No signature found for inputs');
442+
throw new MalformedSignatureError('No corresponding signature for input');
438443
}
439444
if (!isByteSequence(signature[0] as BareItem)) {
440-
throw new Error('Malformed signature');
445+
throw new MalformedSignatureError('Malformed signature');
441446
}
442-
return key.verify(Buffer.from(base), Buffer.from((signature[0] as ByteSequence).toBase64(), 'base64'), Array.from(input[1].entries()).reduce((params, [key, value]) => {
443-
let val: Date | number | string;
444-
switch (key.toLowerCase()) {
445-
case 'created':
446-
case 'expires':
447-
val = new Date((value as number) * 1000);
448-
break;
449-
default: {
450-
if (typeof value === 'string' || typeof value=== 'number') {
451-
val = value;
452-
} else {
453-
val = value.toString();
454-
}
455-
}
456-
}
457-
return Object.assign(params, {
458-
[key]: val,
459-
});
460-
}, {}));
447+
return key.verify(Buffer.from(base), Buffer.from((signature[0] as ByteSequence).toBase64(), 'base64'), signatureParams);
461448
}, Promise.resolve(null));
462449
}

0 commit comments

Comments
 (0)