1
- import { collectHeaders , getHeaderValue } from '../utils.js' ;
1
+ import { collectHeaders , compareUint8Array , getHeaderValue } from '../utils.js' ;
2
2
import { DigestSource , createBase64Digest } from './utils.js' ;
3
3
import type { DigestHashAlgorithm , IncomingRequest } from '../types.js' ;
4
4
import * as sh from 'structured-headers' ;
@@ -39,6 +39,19 @@ function isRFC9530Prefernece(obj: any): obj is RFC9530Prefernece {
39
39
return true ;
40
40
}
41
41
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
+
42
55
/**
43
56
* @param prefernece Prefernece map (Want-*-Digest field parsed by structured-headers.parseDictionary)
44
57
* @param meAcceptable The hash algorithms that You can accept or use
@@ -64,7 +77,7 @@ export function chooseRFC9530HashAlgorithmByPreference(
64
77
return res [ 0 ] ;
65
78
}
66
79
67
- export type RFC9530ResultObject = [ string , [ sh . ByteSequence , Map < any , any > ] ] [ ] ;
80
+ export type RFC9530DigestHeaderObject = [ string , [ sh . ByteSequence , Map < any , any > ] ] [ ] ;
68
81
69
82
/**
70
83
* Generate single Digest header
@@ -74,16 +87,16 @@ export type RFC9530ResultObject = [string, [sh.ByteSequence, Map<any, any>]][];
74
87
* @returns `[[algorithm, [ByteSequence, Map(0)]]]`
75
88
* To convert to string, use serializeDictionary from structured-headers
76
89
*/
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 ) ) {
79
92
throw new RFC9530GenerateDigestHeaderError ( 'Unsupported hash algorithm' ) ;
80
93
}
81
94
return [
82
95
[
83
96
hashAlgorithm . toLowerCase ( ) ,
84
97
[
85
98
new sh . ByteSequence (
86
- await createBase64Digest ( body , hashAlgorithm . toUpperCase ( ) as any )
99
+ await createBase64Digest ( body , convertRFC9530HashAlgorithmToWebCrypto ( hashAlgorithm ) )
87
100
. then ( data => base64 . stringify ( new Uint8Array ( data ) ) )
88
101
) ,
89
102
new Map ( )
@@ -107,7 +120,7 @@ export async function genRFC9530DigestHeader(
107
120
body : DigestSource ,
108
121
hashAlgorithms : string | RFC9530Prefernece | Iterable < string > = [ 'SHA-256' ] ,
109
122
process : 'concurrent' | 'sequential' = 'concurrent' ,
110
- ) : Promise < RFC9530ResultObject > {
123
+ ) : Promise < RFC9530DigestHeaderObject > {
111
124
if ( typeof hashAlgorithms === 'string' ) {
112
125
return await genSingleRFC9530DigestHeader ( body , hashAlgorithms ) ;
113
126
}
@@ -128,63 +141,116 @@ export async function genRFC9530DigestHeader(
128
141
) ) ;
129
142
}
130
143
131
- const result = [ ] as RFC9530ResultObject ;
144
+ const result = [ ] as RFC9530DigestHeaderObject ;
132
145
for ( const algo of hashAlgorithms ) {
133
146
await genSingleRFC9530DigestHeader ( body , algo ) . then ( ( [ v ] ) => result . push ( v ) ) ;
134
147
}
135
148
return result ;
136
149
}
137
150
138
- export const digestHeaderRegEx = / ^ ( [ a - z A - Z 0 - 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 (
141
160
request : IncomingRequest ,
142
161
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
+ } ,
144
186
errorLogger ?: ( ( message : any ) => any )
145
187
) {
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' ) ;
150
193
return false ;
151
194
}
152
195
return true ;
153
196
}
154
197
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' ) ;
158
203
return false ;
159
204
}
160
205
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' ) ;
164
208
return false ;
165
209
}
166
210
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' ) ;
171
214
}
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 ( ', ' ) } )` ) ;
180
218
}
181
- throw e ;
182
219
}
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' ) ;
186
223
return false ;
187
224
}
225
+ if ( ! opts . verifyAll ) {
226
+ hashAlgorithms = [ hashAlgorithms . find ( v => dictionaryAlgorithms . has ( v ) ) ! ] ;
227
+ }
188
228
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
+ }
189
255
return true ;
190
256
}
0 commit comments