Skip to content

Commit 14bb186

Browse files
committed
test
1 parent 0d08a83 commit 14bb186

File tree

3 files changed

+214
-44
lines changed

3 files changed

+214
-44
lines changed

src/digest/digest-rfc3230.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ export async function genRFC3230DigestHeader(body: DigestSource, hashAlgorithm:
99

1010
export const digestHeaderRegEx = /^([a-zA-Z0-9\-]+)=([^\,]+)/;
1111

12+
/**
13+
*
14+
* @param request Incoming request
15+
* @param rawBody Raw body
16+
* @param failOnNoDigest If false, return true when no Digest header is found (default: true)
17+
* @param errorLogger When returing false, called with the error message
18+
* @returns Promise<boolean>
19+
*/
1220
export async function verifyRFC3230DigestHeader(
1321
request: IncomingRequest,
1422
rawBody: DigestSource,

src/digest/digest-rfc9530.test.ts

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
import * as sh from 'structured-headers';
2-
import { RFC9530Prefernece, chooseRFC9530HashAlgorithmByPreference, genRFC9530DigestHeader } from './digest-rfc9530';
2+
import { RFC9530Prefernece, chooseRFC9530HashAlgorithmByPreference, genRFC9530DigestHeader, verifyRFC9530DigestHeader } from './digest-rfc9530';
3+
4+
const base64Resultes = new Map([
5+
['{"hello": "world"}\n', {
6+
'sha-256': 'RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=',
7+
}],
8+
['', {
9+
'sha-256': '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=',
10+
'sha-512': 'z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==',
11+
}],
12+
]);
313

414
describe('rfc9530', () => {
515
test('Want-*-Digest parse', () => {
616
const wantDigest = 'sha-256=10, sha-512=3';
717
const parsed = sh.parseDictionary(wantDigest) as RFC9530Prefernece;
8-
console.log(parsed);
918
expect(parsed).toEqual(new Map([
1019
['sha-256', [10, new Map()]],
1120
['sha-512', [3, new Map()]],
@@ -48,21 +57,108 @@ describe('rfc9530', () => {
4857

4958
describe(genRFC9530DigestHeader, () => {
5059
test('sha-256', async () => {
51-
const result = await genRFC9530DigestHeader('{"hello": "world"}\n', 'sha-256');
60+
const body = '{"hello": "world"}\n';
61+
const result = await genRFC9530DigestHeader(body, 'sha-256');
5262
expect(result).toEqual([
53-
['sha-256', [new sh.ByteSequence('RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg='), new Map()]]
63+
['sha-256', [new sh.ByteSequence(base64Resultes.get(body)?.['sha-256'] as any), new Map()]]
5464
]);
5565
const txt = sh.serializeDictionary(new Map(result));
5666
expect(txt).toBe('sha-256=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=:');
5767
});
5868
test('sha-256, sha-512', async () => {
5969
const result = await genRFC9530DigestHeader('', ['sha-256', 'sha-512']);
6070
expect(result).toEqual([
61-
['sha-256', [new sh.ByteSequence('47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='), new Map()]],
62-
['sha-512', [new sh.ByteSequence('z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg=='), new Map()]],
71+
['sha-256', [new sh.ByteSequence(base64Resultes.get('')?.['sha-256'] as any), new Map()]],
72+
['sha-512', [new sh.ByteSequence(base64Resultes.get('')?.['sha-512'] as any), new Map()]],
6373
]);
6474
const txt = sh.serializeDictionary(new Map(result));
65-
expect(txt).toBe('sha-256=:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=:, sha-512=:z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==:');
75+
expect(txt).toBe(`sha-256=:${base64Resultes.get('')?.['sha-256'] as any}:, sha-512=:${base64Resultes.get('')?.['sha-512'] as any}:`);
76+
});
77+
});
78+
79+
describe(verifyRFC9530DigestHeader, () => {
80+
const body = '{"hello": "world"}\n';
81+
test('normal', async () => {
82+
const request = {
83+
headers: {
84+
'content-digest': `sha-256=:${base64Resultes.get(body)?.['sha-256']}:`,
85+
}
86+
};
87+
expect(await verifyRFC9530DigestHeader(request as any, body)).toBe(true);
88+
});
89+
test('invalid', async () => {
90+
const request = {
91+
headers: {
92+
'content-digest': `sha-256=:${base64Resultes.get('')?.['sha-256']}:`,
93+
}
94+
};
95+
expect(await verifyRFC9530DigestHeader(request as any, body)).toBe(false);
96+
});
97+
test('no digest', async () => {
98+
const request = {
99+
headers: {}
100+
};
101+
expect(await verifyRFC9530DigestHeader(request as any, body)).toBe(false);
102+
});
103+
test('verify many', async () => {
104+
const request = {
105+
headers: {
106+
'content-digest': `sha-256=:${base64Resultes.get('')?.['sha-256']}:, sha-512=:${base64Resultes.get('')?.['sha-512']}:`,
107+
}
108+
};
109+
expect(await verifyRFC9530DigestHeader(request as any, '')).toBe(true);
110+
});
111+
test('verifyAll fail', async () => {
112+
const request = {
113+
headers: {
114+
'content-digest': `sha-256=:${base64Resultes.get('')?.['sha-256']}:, sha-512=:${base64Resultes.get(body)?.['sha-512']}:`,
115+
}
116+
};
117+
expect(await verifyRFC9530DigestHeader(request as any, '')).toBe(false);
118+
});
119+
test('verify one first algorithm ok', async () => {
120+
const request = {
121+
headers: {
122+
'content-digest': `sha-256=:${base64Resultes.get('')?.['sha-256']}:, sha-512=:${base64Resultes.get(body)?.['sha-512']}:`,
123+
}
124+
};
125+
expect(await verifyRFC9530DigestHeader(request as any, '', { verifyAll: false, hashAlgorithms: ['sha-256', 'sha-512'] })).toBe(true);
126+
});
127+
test('verify one first algorithm fail', async () => {
128+
const request = {
129+
headers: {
130+
'content-digest': `sha-256=:${base64Resultes.get('')?.['sha-256']}:, sha-512=:${base64Resultes.get(body)?.['sha-512']}:`,
131+
}
132+
};
133+
expect(await verifyRFC9530DigestHeader(request as any, '', { verifyAll: false, hashAlgorithms: ['sha-512', 'sha-256'] })).toBe(false);
134+
});
135+
test('algorithms missmatch must fail', async () => {
136+
const request = {
137+
headers: {
138+
'content-digest': `sha-512=:${base64Resultes.get('')?.['sha-512']}:`,
139+
}
140+
};
141+
expect(await verifyRFC9530DigestHeader(request as any, '', { hashAlgorithms: ['sha-256'] })).toBe(false);
142+
});
143+
144+
describe('errors', () => {
145+
test('algorithms zero fail', async () => {
146+
const request = {
147+
headers: {
148+
'content-digest': `sha-256=:${base64Resultes.get('')?.['sha-256']}:`,
149+
}
150+
};
151+
expect(verifyRFC9530DigestHeader(request as any, '', { hashAlgorithms: [] })).rejects.toThrow('hashAlgorithms is empty');
152+
});
153+
test('algorithm not supported', async () => {
154+
const request = {
155+
headers: {
156+
'content-digest': `sha-256=:${base64Resultes.get('')?.['sha-256']}:`,
157+
}
158+
};
159+
expect(verifyRFC9530DigestHeader(request as any, '', { hashAlgorithms: ['md5'] })).rejects
160+
.toThrow('Unsupported hash algorithm detected in opts.hashAlgorithms: md5 (supported: sha-256, sha-512)');
161+
});
66162
});
67163
});
68164
});

src/digest/digest-rfc9530.ts

Lines changed: 103 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { collectHeaders, getHeaderValue } from '../utils.js';
1+
import { collectHeaders, compareUint8Array, getHeaderValue } from '../utils.js';
22
import { DigestSource, createBase64Digest } from './utils.js';
33
import type { DigestHashAlgorithm, IncomingRequest } from '../types.js';
44
import * as sh from 'structured-headers';
@@ -39,6 +39,19 @@ function isRFC9530Prefernece(obj: any): obj is RFC9530Prefernece {
3939
return true;
4040
}
4141

42+
const supportedRFC9530HashAlgorithms = ['sha-256', 'sha-512'] satisfies RFC9530HashAlgorithm[];
43+
44+
function isSupportedRFC9530HashAlgorithm(algo: string): algo is RFC9530HashAlgorithm {
45+
return supportedRFC9530HashAlgorithms.includes(algo.toLowerCase() as any);
46+
}
47+
48+
function convertRFC9530HashAlgorithmToWebCrypto(algo: RFC9530HashAlgorithm): DigestHashAlgorithm {
49+
const lowercased = algo.toLowerCase();
50+
if (lowercased === 'sha-256') return 'SHA-256';
51+
if (lowercased === 'sha-512') return 'SHA-512';
52+
throw new Error(`Unsupported hash algorithm: ${lowercased}`);
53+
}
54+
4255
/**
4356
* @param prefernece Prefernece map (Want-*-Digest field parsed by structured-headers.parseDictionary)
4457
* @param meAcceptable The hash algorithms that You can accept or use
@@ -64,7 +77,7 @@ export function chooseRFC9530HashAlgorithmByPreference(
6477
return res[0];
6578
}
6679

67-
export type RFC9530ResultObject = [string, [sh.ByteSequence, Map<any, any>]][];
80+
export type RFC9530DigestHeaderObject = [string, [sh.ByteSequence, Map<any, any>]][];
6881

6982
/**
7083
* Generate single Digest header
@@ -74,16 +87,16 @@ export type RFC9530ResultObject = [string, [sh.ByteSequence, Map<any, any>]][];
7487
* @returns `[[algorithm, [ByteSequence, Map(0)]]]`
7588
* To convert to string, use serializeDictionary from structured-headers
7689
*/
77-
export async function genSingleRFC9530DigestHeader(body: DigestSource, hashAlgorithm: string): Promise<RFC9530ResultObject> {
78-
if (!['SHA-256', 'SHA-512'].includes(hashAlgorithm.toUpperCase())) {
90+
export async function genSingleRFC9530DigestHeader(body: DigestSource, hashAlgorithm: string): Promise<RFC9530DigestHeaderObject> {
91+
if (!isSupportedRFC9530HashAlgorithm(hashAlgorithm)) {
7992
throw new RFC9530GenerateDigestHeaderError('Unsupported hash algorithm');
8093
}
8194
return [
8295
[
8396
hashAlgorithm.toLowerCase(),
8497
[
8598
new sh.ByteSequence(
86-
await createBase64Digest(body, hashAlgorithm.toUpperCase() as any)
99+
await createBase64Digest(body, convertRFC9530HashAlgorithmToWebCrypto(hashAlgorithm))
87100
.then(data => base64.stringify(new Uint8Array(data)))
88101
),
89102
new Map()
@@ -107,7 +120,7 @@ export async function genRFC9530DigestHeader(
107120
body: DigestSource,
108121
hashAlgorithms: string | RFC9530Prefernece | Iterable<string> = ['SHA-256'],
109122
process: 'concurrent' | 'sequential' = 'concurrent',
110-
): Promise<RFC9530ResultObject> {
123+
): Promise<RFC9530DigestHeaderObject> {
111124
if (typeof hashAlgorithms === 'string') {
112125
return await genSingleRFC9530DigestHeader(body, hashAlgorithms);
113126
}
@@ -128,63 +141,116 @@ export async function genRFC9530DigestHeader(
128141
));
129142
}
130143

131-
const result = [] as RFC9530ResultObject;
144+
const result = [] as RFC9530DigestHeaderObject;
132145
for (const algo of hashAlgorithms) {
133146
await genSingleRFC9530DigestHeader(body, algo).then(([v]) => result.push(v));
134147
}
135148
return result;
136149
}
137150

138-
export const digestHeaderRegEx = /^([a-zA-Z0-9\-]+)=([^\,]+)/;
139-
140-
export async function verifyRFC3230DigestHeader(
151+
/**
152+
* Verify Content-Digest header (not Repr-Digest)
153+
* @param request IncomingRequest
154+
* @param rawBody Raw body
155+
* @param opts Options
156+
* @param errorLogger Error logger when verification fails
157+
* @returns Whether digest is valid with the body
158+
*/
159+
export async function verifyRFC9530DigestHeader(
141160
request: IncomingRequest,
142161
rawBody: DigestSource,
143-
failOnNoDigest = true,
162+
opts: {
163+
/**
164+
* If false, return true when no Digest header is found
165+
* @default true
166+
*/
167+
failOnNoDigest?: boolean,
168+
/**
169+
* If true, verify all digests without not supported algorithms
170+
* If false, use the first supported and exisiting algorithm in hashAlgorithms
171+
* @default true
172+
*/
173+
verifyAll?: boolean,
174+
/**
175+
* Specify hash algorithms you accept. (RFC 9530 algorithm registries)
176+
*
177+
* If `varifyAll: false`, it is also used to choose the hash algorithm to verify.
178+
* (Younger index is preferred.)
179+
*/
180+
hashAlgorithms?: RFC9530HashAlgorithm[],
181+
} = {
182+
failOnNoDigest: true,
183+
verifyAll: true,
184+
hashAlgorithms: ['sha-256', 'sha-512'],
185+
},
144186
errorLogger?: ((message: any) => any)
145187
) {
146-
const digestHeader = getHeaderValue(collectHeaders(request), 'digest');
147-
if (!digestHeader) {
148-
if (failOnNoDigest) {
149-
if (errorLogger) errorLogger('Digest header not found');
188+
const headers = collectHeaders(request);
189+
const contentDigestHeader = getHeaderValue(headers, 'content-digest');
190+
if (!contentDigestHeader) {
191+
if (opts.failOnNoDigest) {
192+
if (errorLogger) errorLogger('Repr-Digest or Content-Digest header not found');
150193
return false;
151194
}
152195
return true;
153196
}
154197

155-
const match = digestHeader.match(digestHeaderRegEx);
156-
if (!match) {
157-
if (errorLogger) errorLogger('Invalid Digest header format');
198+
let dictionary: RFC9530DigestHeaderObject;
199+
try {
200+
dictionary = Array.from(sh.parseDictionary(contentDigestHeader), ([k, v]) => [k.toLowerCase(), v]) as RFC9530DigestHeaderObject;
201+
} catch (e: any) {
202+
if (errorLogger) errorLogger('Invalid Digest header');
158203
return false;
159204
}
160205

161-
const value = match[2];
162-
if (!value) {
163-
if (errorLogger) errorLogger('Invalid Digest header format');
206+
if (dictionary.length === 0) {
207+
if (errorLogger) errorLogger('Digest header is empty');
164208
return false;
165209
}
166210

167-
const algo = match[1] as DigestHashAlgorithm;
168-
if (!algo) {
169-
if (errorLogger) errorLogger(`Invalid Digest header algorithm: ${match[1]}`);
170-
return false;
211+
let hashAlgorithms = (opts.hashAlgorithms || ['sha-256', 'sha-512']).map(v => v.toLowerCase());
212+
if (hashAlgorithms.length === 0) {
213+
throw new Error('hashAlgorithms is empty');
171214
}
172-
173-
let hash: string;
174-
try {
175-
hash = await createBase64Digest(rawBody, algo);
176-
} catch (e: any) {
177-
if (e.name === 'NotSupportedError') {
178-
if (errorLogger) errorLogger(`Invalid Digest header algorithm: ${algo}`);
179-
return false;
215+
for (const algo of hashAlgorithms) {
216+
if (!isSupportedRFC9530HashAlgorithm(algo)) {
217+
throw new Error(`Unsupported hash algorithm detected in opts.hashAlgorithms: ${algo} (supported: ${supportedHashAlgorithmsWithRFC9530AndWebCrypto.join(', ')})`);
180218
}
181-
throw e;
182219
}
183-
184-
if (hash !== value) {
185-
if (errorLogger) errorLogger(`Digest header hash mismatch`);
220+
const dictionaryAlgorithms = dictionary.reduce((prev, [k]) => prev.add(k), new Set<string>());
221+
if (!hashAlgorithms.some(v => dictionaryAlgorithms.has(v))) {
222+
if (errorLogger) errorLogger('No supported Content-Digest header algorithm');
186223
return false;
187224
}
225+
if (!opts.verifyAll) {
226+
hashAlgorithms = [hashAlgorithms.find(v => dictionaryAlgorithms.has(v))!];
227+
}
188228

229+
const results = await Promise.allSettled(
230+
dictionary.map(([algo, [value]]) => {
231+
if (!hashAlgorithms.includes(algo.toLowerCase() as RFC9530HashAlgorithm)) {
232+
return Promise.resolve(null);
233+
}
234+
if (!(value instanceof sh.ByteSequence)) {
235+
return Promise.reject(new Error('Invalid dictionary value type'));
236+
}
237+
return createBase64Digest(rawBody, convertRFC9530HashAlgorithmToWebCrypto(algo.toLowerCase() as RFC9530HashAlgorithm))
238+
.then(hash => compareUint8Array(base64.parse(value.toBase64()), new Uint8Array(hash)));
239+
})
240+
);
241+
if (!results.some(v => v.status === 'fulfilled' && v.value === true)) {
242+
// 全部fullfilled, nullだとtrueになってしまうので
243+
if (errorLogger) errorLogger(`No digest(s) matched`);
244+
return false;
245+
}
246+
for (const result of results) {
247+
if (result.status === 'fulfilled' && result.value === false) {
248+
if (errorLogger) errorLogger(`Content-Digest header hash simply mismatched`);
249+
return false;
250+
} else if (result.status === 'rejected') {
251+
if (errorLogger) errorLogger(`Content-Digest header parse error: ${result.reason}`);
252+
return false;
253+
}
254+
}
189255
return true;
190256
}

0 commit comments

Comments
 (0)